A store record is a name and a catalog

Both extras go. `config` was added this morning on the idea that the registry should
say how each store behaves; that was wrong. The configuration is fixed per installed
client — the client carries its own store engine and knows its own machine — so a copy
here was a second authority over decisions the client had already made correctly,
including which directories it may delete from. Keeping two stores on one machine apart
is a subfolder, and the client derives that itself.

`store_repository_url` goes with it. The store engines it pointed at no longer exist, and
a URL nobody follows is a URL that goes stale.

The public stores page loses its desktop card for the same reason: it advertised a
`curl … | sh` for a repository that is gone, and an ordinary computer is served by the
app in the section above it. `/desktop` now lands on that app rather than on a device
tab, so the old URL still means what someone typing it wants.

Unrelated but in the way: the dead `engines` list in that page has been failing
`vue-tsc` on master, so the frontend could not be built to check any of this. Removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-19 07:19:52 +02:00
co-authored by Claude Opus 5
parent 9255a11254
commit 021f2b9b07
14 changed files with 51 additions and 245 deletions
@@ -19,36 +19,10 @@ RSpec.describe Api::StoresController, type: :request do
get "/api/stores"
store = JSON.parse(response.body).first
expect(store).to include("name", "catalogUrl", "storeRepositoryUrl", "config")
# Exactly two fields: a name and a catalog are the whole record, and a client
# that starts reading a third would be reading something this site no longer says.
expect(store.keys).to contain_exactly("name", "catalogUrl")
expect(store["catalogUrl"]).to eq("https://teletypegames.org")
expect(store["storeRepositoryUrl"]).to end_with("/stores/ttg-desktop-store")
end
# The config is what the client applies instead of fetching a repository's
# config.json, so it has to come back as an object rather than as JSON text.
it "carries the store's own config as an object" do
create(:store, config: { "paths" => { "subfolder" => "teletypegames" } })
get "/api/stores"
config = JSON.parse(response.body).first["config"]
expect(config).to eq("paths" => { "subfolder" => "teletypegames" })
end
it "answers null for a store that configures nothing" do
create(:store, config: nil)
get "/api/stores"
expect(JSON.parse(response.body).first["config"]).to be_nil
end
it "answers null for a store with no repository" do
create(:store, store_repository_url: nil)
get "/api/stores"
expect(JSON.parse(response.body).first["storeRepositoryUrl"]).to be_nil
end
it "leaves out soft-deleted stores" do
-1
View File
@@ -2,6 +2,5 @@ FactoryBot.define do
factory :store do
name { "Teletype Games" }
catalog_url { "https://teletypegames.org" }
store_repository_url { "https://git.teletypegames.org/stores/ttg-desktop-store" }
end
end
-57
View File
@@ -10,63 +10,6 @@ RSpec.describe Store, type: :model do
expect(store.errors[:catalog_url]).to include("must be an http(s) URL")
end
it "rejects a repository url that is not http(s)" do
expect(build(:store, store_repository_url: "not a url")).not_to be_valid
end
# A store without a repository is a complete store: the client takes identity
# from the record and everything else from the engine's defaults.
it "accepts a store with no repository at all" do
expect(build(:store, store_repository_url: nil)).to be_valid
expect(build(:store, store_repository_url: "")).to be_valid
end
describe "config" do
it "accepts a store that configures nothing" do
expect(build(:store, config: nil)).to be_valid
end
it "rejects a config that is not a JSON object" do
store = build(:store, config: ["not", "an", "object"])
expect(store).not_to be_valid
expect(store.errors[:config]).to include("must be a JSON object")
end
# The admin form edits JSON text; the column holds real JSON. These two accessors
# are the only bridge between them, so both directions are worth pinning.
it "parses config_json into the column" do
store = build(:store, config_json: '{"paths": {"subfolder": "teletypegames"}}')
expect(store).to be_valid
expect(store.config).to eq("paths" => { "subfolder" => "teletypegames" })
end
it "renders the column back as pretty JSON text" do
store = build(:store, config: { "paths" => { "subfolder" => "teletypegames" } })
expect(JSON.parse(store.config_json)).to eq(store.config)
expect(store.config_json).to include("\n")
end
it "clears the config when the text is emptied" do
store = build(:store, config: { "paths" => {} })
store.config_json = ""
expect(store).to be_valid
expect(store.config).to be_nil
end
# Refused with the text kept, rather than raising: the form has to come back with
# what the person typed and a message they can act on.
it "refuses invalid JSON and keeps the text" do
store = build(:store, config_json: '{"paths": ')
expect(store).not_to be_valid
expect(store.errors[:config].first).to start_with("is not valid JSON")
expect(store.config_json).to eq('{"paths": ')
end
end
describe ".ordered" do
it "lists stores by name" do
later = create(:store, name: "Zed Games")