Phase 0: in-place decoupling before WarpEngine extraction

- FileService/DownloadService: lazy container-path resolution instead of
  class-load-time realpath (boot no longer requires /softwares to exist)
- downloadCount/totalDownloads: single grouped query passed as Blueprinter
  option instead of per-release association counts (fixes N+1)
- admin Images: Member coupling replaced with ImageUsage owner registry
  (precursor of the WarpEngine.config.image_owners hook)
- Image: UPLOAD_PATH constant replaced with call-time upload_path accessor
- proper test environment (config/environments/test.rb, softwares_test DB,
  hosts.clear) — suite previously ran against the development DB
- spec fixes: case-insensitive uniqueness matchers (MySQL ai_ci collation),
  DB-cascade has_many expectations, PlatformLink::SUPPORTED_PLATFORMS
- JSON baselines of /api/software and /api/builds for post-extraction diffing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 18:39:20 +02:00
co-authored by Claude Fable 5
parent 0f13c19fc8
commit bf921c589d
19 changed files with 851 additions and 32 deletions
@@ -13,6 +13,23 @@ RSpec.describe Api::SoftwareController, type: :request do
expect(json["softwares"].length).to eq(1)
end
it "returns per-release and total download counts" do
software = create(:software)
release = create(:release, software: software)
other = create(:release, software: software)
create_list(:download, 3, release: release)
create(:download, release: other)
get "/api/software"
json = JSON.parse(response.body)
sw = json["softwares"].first
expect(sw["totalDownloads"]).to eq(4)
counts = sw["releases"].to_h { |r| [ r["id"], r["downloadCount"] ] }
expect(counts[release.id]).to eq(3)
expect(counts[other.id]).to eq(1)
end
it "excludes soft-deleted software" do
create(:software, deleted_at: Time.current)
+4 -1
View File
@@ -1,6 +1,9 @@
require "rails_helper"
RSpec.describe Member, type: :model do
subject { build(:member) }
it { should validate_presence_of(:nick) }
it { should validate_uniqueness_of(:nick) }
# MySQL utf8mb4_0900_ai_ci collation: az egyediség DB-szinten case-insensitive
it { should validate_uniqueness_of(:nick).case_insensitive }
end
+7 -3
View File
@@ -1,13 +1,17 @@
require "rails_helper"
RSpec.describe Software, type: :model do
subject { build(:software) }
it { should validate_presence_of(:name) }
it { should validate_presence_of(:title) }
it { should validate_presence_of(:platform) }
it { should validate_uniqueness_of(:name) }
# MySQL utf8mb4_0900_ai_ci collation: az egyediség DB-szinten case-insensitive
it { should validate_uniqueness_of(:name).case_insensitive }
it { should have_many(:releases).dependent(:destroy) }
it { should have_many(:external_links).dependent(:destroy) }
# a törlést a DB-szintű ON DELETE CASCADE végzi, a modellen nincs dependent opció
it { should have_many(:releases) }
it { should have_many(:external_links) }
it { should have_many(:software_images).dependent(:destroy) }
describe "default scope" do
@@ -19,7 +19,7 @@ RSpec.describe BuildsService do
end
it "includes all supported platforms" do
UpdateService::SUPPORTED_PLATFORMS.each do |platform|
PlatformLink::SUPPORTED_PLATFORMS.each do |platform|
expect(result[:platforms]).to have_key(platform)
end
end
@@ -0,0 +1,79 @@
{
"platforms": {
"tic80": {
"label": "TIC-80",
"kinds": [
"cartridge",
"source",
"html",
"docs",
"win_x64",
"linux_x64",
"mac_x64"
]
},
"ebitengine": {
"label": "Ebitengine",
"kinds": [
"html",
"win_x86",
"win_x64",
"linux_x64",
"mac_x64",
"mac_arm64"
]
},
"love": {
"label": "L\u00d6VE",
"kinds": [
"html",
"win_x64",
"linux_x64",
"mac_universal"
]
},
"c64": {
"label": "C64",
"kinds": [
"cartridge"
]
},
"godot": {
"label": "Godot",
"kinds": [
"html",
"win_x86",
"win_x64",
"linux_x64",
"mac_universal"
]
},
"bevy": {
"label": "Bevy",
"kinds": [
"html",
"win_x64",
"linux_x64"
]
},
"phaser": {
"label": "Phaser",
"kinds": [
"html"
]
}
},
"allKinds": [
"cartridge",
"source",
"html",
"docs",
"win_x86",
"win_x64",
"linux_x86",
"linux_x64",
"mac_x64",
"mac_arm64",
"mac_universal"
]
}
@@ -0,0 +1,629 @@
{
"softwares": [
{
"latestRelease": {
"assets": [
{
"kind": "cartridge",
"path": "/file/impostor-1.0.tic"
},
{
"kind": "source",
"path": "/file/impostor-1.0.lua"
},
{
"kind": "html",
"path": "/file/impostor-1.0"
},
{
"kind": "docs",
"path": "/file/impostor-1.0-docs"
}
],
"cartridgePath": "/file/impostor-1.0.tic",
"createdAt": "2026-04-30T15:56:12.781Z",
"deletedAt": null,
"docsFolderPath": "/file/impostor-1.0-docs",
"downloadCount": 0,
"htmlFolderPath": "/file/impostor-1.0",
"id": 96,
"softwareId": 19,
"sourcePath": "/file/impostor-1.0.lua",
"updatedAt": "2026-04-30T15:56:12.781Z",
"version": "1.0"
},
"releases": [
{
"assets": [
{
"kind": "cartridge",
"path": "/file/impostor-1.0.tic"
},
{
"kind": "source",
"path": "/file/impostor-1.0.lua"
},
{
"kind": "html",
"path": "/file/impostor-1.0"
},
{
"kind": "docs",
"path": "/file/impostor-1.0-docs"
}
],
"cartridgePath": "/file/impostor-1.0.tic",
"createdAt": "2026-04-30T15:56:12.781Z",
"deletedAt": null,
"docsFolderPath": "/file/impostor-1.0-docs",
"downloadCount": 0,
"htmlFolderPath": "/file/impostor-1.0",
"id": 96,
"softwareId": 19,
"sourcePath": "/file/impostor-1.0.lua",
"updatedAt": "2026-04-30T15:56:12.781Z",
"version": "1.0"
},
{
"assets": [
{
"kind": "cartridge",
"path": "/file/impostor-1.0-beta2.tic"
},
{
"kind": "source",
"path": "/file/impostor-1.0-beta2.lua"
},
{
"kind": "html",
"path": "/file/impostor-1.0-beta2"
},
{
"kind": "docs",
"path": "/file/impostor-1.0-beta2-docs"
}
],
"cartridgePath": "/file/impostor-1.0-beta2.tic",
"createdAt": "2026-04-09T19:55:42.764Z",
"deletedAt": null,
"docsFolderPath": "/file/impostor-1.0-beta2-docs",
"downloadCount": 0,
"htmlFolderPath": "/file/impostor-1.0-beta2",
"id": 85,
"softwareId": 19,
"sourcePath": "/file/impostor-1.0-beta2.lua",
"updatedAt": "2026-04-09T19:55:42.764Z",
"version": "1.0-beta2"
},
{
"assets": [
{
"kind": "cartridge",
"path": "/file/impostor-1.0-beta1.tic"
},
{
"kind": "source",
"path": "/file/impostor-1.0-beta1.lua"
},
{
"kind": "html",
"path": "/file/impostor-1.0-beta1"
},
{
"kind": "docs",
"path": "/file/impostor-1.0-beta1-docs"
}
],
"cartridgePath": "/file/impostor-1.0-beta1.tic",
"createdAt": "2026-03-22T22:49:54.926Z",
"deletedAt": null,
"docsFolderPath": "/file/impostor-1.0-beta1-docs",
"downloadCount": 0,
"htmlFolderPath": "/file/impostor-1.0-beta1",
"id": 77,
"softwareId": 19,
"sourcePath": "/file/impostor-1.0-beta1.lua",
"updatedAt": "2026-03-22T22:49:54.926Z",
"version": "1.0-beta1"
},
{
"assets": [
{
"kind": "cartridge",
"path": "/file/impostor-1.0-alpha.tic"
},
{
"kind": "source",
"path": "/file/impostor-1.0-alpha.lua"
},
{
"kind": "html",
"path": "/file/impostor-1.0-alpha"
},
{
"kind": "docs",
"path": "/file/impostor-1.0-alpha-docs"
}
],
"cartridgePath": "/file/impostor-1.0-alpha.tic",
"createdAt": "2026-03-20T14:31:12.319Z",
"deletedAt": null,
"docsFolderPath": "/file/impostor-1.0-alpha-docs",
"downloadCount": 0,
"htmlFolderPath": "/file/impostor-1.0-alpha",
"id": 71,
"softwareId": 19,
"sourcePath": "/file/impostor-1.0-alpha.lua",
"updatedAt": "2026-03-20T14:31:12.319Z",
"version": "1.0-alpha"
}
],
"software": {
"author": "Teletype Games",
"createdAt": "2026-03-04T14:04:42.676Z",
"deletedAt": null,
"desc": "Life of a programmer",
"externalLinks": [
{
"createdAt": "0001-01-01T00:00:00Z",
"deletedAt": null,
"id": 1,
"label": "Source Code",
"softwareId": 19,
"updatedAt": "0001-01-01T00:00:00Z",
"url": "https://git.teletype.hu/games/impostor/"
},
{
"createdAt": "0001-01-01T00:00:00Z",
"deletedAt": null,
"id": 2,
"label": "Itch.io page",
"softwareId": 19,
"updatedAt": "0001-01-01T00:00:00Z",
"url": "https://teletypegames.itch.io/definitely-not-an-impostor"
}
],
"highlighted": true,
"id": 19,
"imageUrl": null,
"images": [],
"license": "MIT License",
"name": "impostor",
"platform": "tic80",
"platformLinks": [],
"status": "released",
"story": "Norman is an average simulation engineer living a dull but stable life. Hes exactly where he belongsthough he doesnt always feel that way. Follow his mostly work-driven life and discover the excitement within it. Just dont be surprised if you find more than expected.",
"title": "Definitely not an Impostor",
"updatedAt": "2026-04-30T21:15:03.809Z"
},
"totalDownloads": 0,
"webPlayableRelease": {
"assets": [
{
"kind": "cartridge",
"path": "/file/impostor-1.0.tic"
},
{
"kind": "source",
"path": "/file/impostor-1.0.lua"
},
{
"kind": "html",
"path": "/file/impostor-1.0"
},
{
"kind": "docs",
"path": "/file/impostor-1.0-docs"
}
],
"cartridgePath": "/file/impostor-1.0.tic",
"createdAt": "2026-04-30T15:56:12.781Z",
"deletedAt": null,
"docsFolderPath": "/file/impostor-1.0-docs",
"downloadCount": 0,
"htmlFolderPath": "/file/impostor-1.0",
"id": 96,
"softwareId": 19,
"sourcePath": "/file/impostor-1.0.lua",
"updatedAt": "2026-04-30T15:56:12.781Z",
"version": "1.0"
}
},
{
"latestRelease": {
"assets": [
{
"kind": "cartridge",
"path": "/file/bombexpert-0.2.tic"
},
{
"kind": "source",
"path": "/file/bombexpert-0.2.lua"
},
{
"kind": "html",
"path": "/file/bombexpert-0.2"
},
{
"kind": "docs",
"path": "/file/bombexpert-0.2-docs"
}
],
"cartridgePath": "/file/bombexpert-0.2.tic",
"createdAt": "2026-03-04T14:05:54.728Z",
"deletedAt": null,
"docsFolderPath": "/file/bombexpert-0.2-docs",
"downloadCount": 0,
"htmlFolderPath": "/file/bombexpert-0.2",
"id": 52,
"softwareId": 20,
"sourcePath": "/file/bombexpert-0.2.lua",
"updatedAt": "2026-03-04T14:05:54.728Z",
"version": "0.2"
},
"releases": [
{
"assets": [
{
"kind": "cartridge",
"path": "/file/bombexpert-0.2.tic"
},
{
"kind": "source",
"path": "/file/bombexpert-0.2.lua"
},
{
"kind": "html",
"path": "/file/bombexpert-0.2"
},
{
"kind": "docs",
"path": "/file/bombexpert-0.2-docs"
}
],
"cartridgePath": "/file/bombexpert-0.2.tic",
"createdAt": "2026-03-04T14:05:54.728Z",
"deletedAt": null,
"docsFolderPath": "/file/bombexpert-0.2-docs",
"downloadCount": 0,
"htmlFolderPath": "/file/bombexpert-0.2",
"id": 52,
"softwareId": 20,
"sourcePath": "/file/bombexpert-0.2.lua",
"updatedAt": "2026-03-04T14:05:54.728Z",
"version": "0.2"
}
],
"software": {
"author": "Zsolt Tasnadi",
"createdAt": "2026-03-04T14:05:54.725Z",
"deletedAt": null,
"desc": "Simple BombExpert for TIC-80",
"externalLinks": [],
"highlighted": false,
"id": 20,
"imageUrl": null,
"images": [],
"license": "MIT License",
"name": "bombexpert",
"platform": "tic80",
"platformLinks": [],
"status": "archived",
"story": "",
"title": "BombExpert",
"updatedAt": "2026-03-04T14:05:54.725Z"
},
"totalDownloads": 0,
"webPlayableRelease": {
"assets": [
{
"kind": "cartridge",
"path": "/file/bombexpert-0.2.tic"
},
{
"kind": "source",
"path": "/file/bombexpert-0.2.lua"
},
{
"kind": "html",
"path": "/file/bombexpert-0.2"
},
{
"kind": "docs",
"path": "/file/bombexpert-0.2-docs"
}
],
"cartridgePath": "/file/bombexpert-0.2.tic",
"createdAt": "2026-03-04T14:05:54.728Z",
"deletedAt": null,
"docsFolderPath": "/file/bombexpert-0.2-docs",
"downloadCount": 0,
"htmlFolderPath": "/file/bombexpert-0.2",
"id": 52,
"softwareId": 20,
"sourcePath": "/file/bombexpert-0.2.lua",
"updatedAt": "2026-03-04T14:05:54.728Z",
"version": "0.2"
}
},
{
"latestRelease": {
"assets": [
{
"kind": "html",
"path": "/file/ebitenginedemo-1.0.0"
}
],
"cartridgePath": "",
"createdAt": "2026-03-04T14:06:36.626Z",
"deletedAt": null,
"docsFolderPath": "",
"downloadCount": 0,
"htmlFolderPath": "/file/ebitenginedemo-1.0.0",
"id": 53,
"softwareId": 21,
"sourcePath": "",
"updatedAt": "2026-03-04T14:06:36.626Z",
"version": "1.0.0"
},
"releases": [
{
"assets": [
{
"kind": "html",
"path": "/file/ebitenginedemo-1.0.0"
}
],
"cartridgePath": "",
"createdAt": "2026-03-04T14:06:36.626Z",
"deletedAt": null,
"docsFolderPath": "",
"downloadCount": 0,
"htmlFolderPath": "/file/ebitenginedemo-1.0.0",
"id": 53,
"softwareId": 21,
"sourcePath": "",
"updatedAt": "2026-03-04T14:06:36.626Z",
"version": "1.0.0"
}
],
"software": {
"author": "Teletype Games",
"createdAt": "2026-03-04T14:06:36.623Z",
"deletedAt": null,
"desc": "It's a simple demo program in Ebitengine",
"externalLinks": [],
"highlighted": false,
"id": 21,
"imageUrl": null,
"images": [],
"license": "MIT License",
"name": "ebitenginedemo",
"platform": "ebitengine",
"platformLinks": [],
"status": "demo",
"story": "",
"title": "Ebitengine Demo",
"updatedAt": "2026-03-04T14:06:36.623Z"
},
"totalDownloads": 0,
"webPlayableRelease": {
"assets": [
{
"kind": "html",
"path": "/file/ebitenginedemo-1.0.0"
}
],
"cartridgePath": "",
"createdAt": "2026-03-04T14:06:36.626Z",
"deletedAt": null,
"docsFolderPath": "",
"downloadCount": 0,
"htmlFolderPath": "/file/ebitenginedemo-1.0.0",
"id": 53,
"softwareId": 21,
"sourcePath": "",
"updatedAt": "2026-03-04T14:06:36.626Z",
"version": "1.0.0"
}
},
{
"latestRelease": {
"assets": [
{
"kind": "cartridge",
"path": "/file/mranderson-0.1.tic"
},
{
"kind": "source",
"path": "/file/mranderson-0.1.lua"
},
{
"kind": "html",
"path": "/file/mranderson-0.1"
},
{
"kind": "docs",
"path": "/file/mranderson-0.1-docs"
}
],
"cartridgePath": "/file/mranderson-0.1.tic",
"createdAt": "2026-03-04T14:06:58.912Z",
"deletedAt": null,
"docsFolderPath": "/file/mranderson-0.1-docs",
"downloadCount": 0,
"htmlFolderPath": "/file/mranderson-0.1",
"id": 54,
"softwareId": 22,
"sourcePath": "/file/mranderson-0.1.lua",
"updatedAt": "2026-03-04T14:06:58.912Z",
"version": "0.1"
},
"releases": [
{
"assets": [
{
"kind": "cartridge",
"path": "/file/mranderson-0.1.tic"
},
{
"kind": "source",
"path": "/file/mranderson-0.1.lua"
},
{
"kind": "html",
"path": "/file/mranderson-0.1"
},
{
"kind": "docs",
"path": "/file/mranderson-0.1-docs"
}
],
"cartridgePath": "/file/mranderson-0.1.tic",
"createdAt": "2026-03-04T14:06:58.912Z",
"deletedAt": null,
"docsFolderPath": "/file/mranderson-0.1-docs",
"downloadCount": 0,
"htmlFolderPath": "/file/mranderson-0.1",
"id": 54,
"softwareId": 22,
"sourcePath": "/file/mranderson-0.1.lua",
"updatedAt": "2026-03-04T14:06:58.912Z",
"version": "0.1"
}
],
"software": {
"author": "Zsolt Tasnadi",
"createdAt": "2026-03-04T14:06:58.907Z",
"deletedAt": null,
"desc": "Life of a programmer in the Vector",
"externalLinks": [],
"highlighted": false,
"id": 22,
"imageUrl": null,
"images": [],
"license": "MIT License",
"name": "mranderson",
"platform": "tic80",
"platformLinks": [],
"status": "development",
"story": "",
"title": "Mr Anderson's Adventure",
"updatedAt": "2026-03-04T14:06:58.907Z"
},
"totalDownloads": 0,
"webPlayableRelease": {
"assets": [
{
"kind": "cartridge",
"path": "/file/mranderson-0.1.tic"
},
{
"kind": "source",
"path": "/file/mranderson-0.1.lua"
},
{
"kind": "html",
"path": "/file/mranderson-0.1"
},
{
"kind": "docs",
"path": "/file/mranderson-0.1-docs"
}
],
"cartridgePath": "/file/mranderson-0.1.tic",
"createdAt": "2026-03-04T14:06:58.912Z",
"deletedAt": null,
"docsFolderPath": "/file/mranderson-0.1-docs",
"downloadCount": 0,
"htmlFolderPath": "/file/mranderson-0.1",
"id": 54,
"softwareId": 22,
"sourcePath": "/file/mranderson-0.1.lua",
"updatedAt": "2026-03-04T14:06:58.912Z",
"version": "0.1"
}
},
{
"latestRelease": {
"assets": [
{
"kind": "html",
"path": "/file/love2ddemo-1.0.0"
}
],
"cartridgePath": "",
"createdAt": "2026-03-04T14:09:14.425Z",
"deletedAt": null,
"docsFolderPath": "",
"downloadCount": 0,
"htmlFolderPath": "/file/love2ddemo-1.0.0",
"id": 55,
"softwareId": 23,
"sourcePath": "",
"updatedAt": "2026-03-04T14:09:14.425Z",
"version": "1.0.0"
},
"releases": [
{
"assets": [
{
"kind": "html",
"path": "/file/love2ddemo-1.0.0"
}
],
"cartridgePath": "",
"createdAt": "2026-03-04T14:09:14.425Z",
"deletedAt": null,
"docsFolderPath": "",
"downloadCount": 0,
"htmlFolderPath": "/file/love2ddemo-1.0.0",
"id": 55,
"softwareId": 23,
"sourcePath": "",
"updatedAt": "2026-03-04T14:09:14.425Z",
"version": "1.0.0"
}
],
"software": {
"author": "Teletype Games",
"createdAt": "2026-03-04T14:09:14.421Z",
"deletedAt": null,
"desc": "It's a simple demo program in Love2D",
"externalLinks": [],
"highlighted": false,
"id": 23,
"imageUrl": null,
"images": [],
"license": "MIT License",
"name": "love2ddemo",
"platform": "love",
"platformLinks": [],
"status": "demo",
"story": "",
"title": "Love2D Demo",
"updatedAt": "2026-03-04T14:11:51.053Z"
},
"totalDownloads": 0,
"webPlayableRelease": {
"assets": [
{
"kind": "html",
"path": "/file/love2ddemo-1.0.0"
}
],
"cartridgePath": "",
"createdAt": "2026-03-04T14:09:14.425Z",
"deletedAt": null,
"docsFolderPath": "",
"downloadCount": 0,
"htmlFolderPath": "/file/love2ddemo-1.0.0",
"id": 55,
"softwareId": 23,
"sourcePath": "",
"updatedAt": "2026-03-04T14:09:14.425Z",
"version": "1.0.0"
}
}
]
}