api Mod Controller Compatibility Info and Required Disclosures system#138
api Mod Controller Compatibility Info and Required Disclosures system#138budak7273 wants to merge 95 commits into
Conversation
- update devcontainer.json to customizations spec - switch to modern container repository and pin to 1.22-bullseye (same as go.mod) - install protoc as part of container setup, and its deps https://github.com/ericslandry/grpc/blob/dcc9b228a2189933b0666eeea23852a12225092b/.devcontainer/post-create.sh
note: must go directly to .56 because of this bug 99designs/gqlgen#3321
(I uncommented the toolchain directive and tidy removed it)
…n hashes are consistent
…e progress=plain for ci
``` failed parsing config: failed to parse configFile /etc/tempo.yaml: yaml: unmarshal errors: line 31: field compactor not found in type app.Config ```
…lt in any generated changes?)
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## staging #138 +/- ##
===========================================
+ Coverage 41.73% 42.23% +0.50%
===========================================
Files 128 128
Lines 6146 6203 +57
===========================================
+ Hits 2565 2620 +55
+ Misses 3191 3185 -6
- Partials 390 398 +8 ☔ View full report in Codecov by Sentry. |
|
@claude review |
There was a problem hiding this comment.
(Bot didn't work when I tagged it, so I had Claude Code run it, here is the result of that:)
Did a thorough pass over the hand-written changes (excluding generated/ and mise.lock). Most of the PR looks great — the disclosure plumbing is well-placed and the new gating correctly rides the already-fetched mod row instead of adding a DB round-trip. A few things to address before merging though, plus some cleanup suggestions.
🔴 Ship-blockers
1. Nil-pointer panic in UpdateMod AI disclosure handling
gql/resolver_mods.go:197
if requiresMessage(aiDisclosureUpdate.DisclosureType) && *aiDisclosureUpdate.DisclosureString == "" {schemas/mod.graphql declares disclosure_string: String (nullable), so aiDisclosureUpdate.DisclosureString is *string. A client sending {disclosure_type: ai_usage} without a disclosure_string will crash the resolver on the deref.
// suggested
if requiresMessage(aiDisclosureUpdate.DisclosureType) &&
(aiDisclosureUpdate.DisclosureString == nil || *aiDisclosureUpdate.DisclosureString == "") {2. requiresMessage disagrees with the version-upload gate
gql/resolver_mods.go:162-164 says a message is required only for ai_usage / runtime_ai_usage.
gql/resolver_versions.go:54-60 treats anything != NoAiUsage (i.e. including no_disclosure) as "needs a message" before unblocking uploads.
So no_disclosure is a valid persisted state from UpdateMod's perspective but a blocker from CreateVersion's perspective. Pick one definition of "set" and centralize it — currently each resolver answers a slightly different question.
3. testza.AssertNoError uses token as the failure message
tests/version_test.go:112
token, _, err = makeUser(ctx)
testza.AssertNoError(t, err, token)token is being passed as msg ...interface{}. If makeUser fails, the test failure will print the (likely empty) token rather than something useful. Drop the third arg.
🟠 Concrete cleanup
4. Controller-default migration does two UPDATEs on the same rows
migrations/sql/20260501041455_add_default_controller_compatibility.up.sql
The first statement writes {"Controller":""}, the second overwrites it with {"Note":"","State":"Untested"}. Every matching row is rewritten twice (2× WAL, 2× heap update, 2× lock window) and the intermediate Controller: "" is semantically an invalid Compatibility. Collapses to one statement:
UPDATE "mods"
SET "compatibility" = jsonb_set(compatibility, '{Controller}',
'{"Note":"","State":"Untested"}'::jsonb)
WHERE NOT compatibility ? 'Controller' AND compatibility ? 'EA';5. util.AIUseDisclosureInfo is stringly-typed
util/db.go:30-33
type AIUseDisclosureInfo struct {
DisclosureType string
DisclosureString string
}Forces .String() / raw-string comparisons at gql/resolver_versions.go:58 (!= generated.AIUseDisclosureTypeNoAiUsage.String()) and gql/resolver_mods.go:201. The existing util.Stability (top of the same file) is the established pattern for a typed enum-string:
type AIUseDisclosureType string
const ( AIUseNoDisclosure AIUseDisclosureType = "no_disclosure" /* ... */ )Drops every .String() dance and lets go vet catch typos.
6. GenControllerCompToDBControllerComp duplicates GenCompToDBComp
gql/gql_types.go:27 — same util.Compatibility{State, Note} build from a near-identical generated input. Either generalize with generics over a small interface{ GetState() string; GetNote() *string }, or share one input type in the schema (controller-specific enum on a separate field).
7. Two Count() queries in the extract activity
workflows/versionupload/extract_mod_info.go:54-77 — the new soft-deleted check sits right next to the existing live check on the same (mod_id, version) key. Two Postgres round-trips back-to-back. Both are presence-only — Exist(ctx) is cheaper than Count(ctx) (SELECT 1 LIMIT 1 vs aggregate), or merge into a single SkipSoftDelete query partitioned in Go.
8. Duplicated default-disclosure test setup
tests/version_test.go:156 and tests/sml_versions_test.go:77 — near-verbatim mutation block setting AiUseDisclosure: NoAiUsage so uploads aren't blocked. Belongs as a helper in tests/utils.go next to makeUser — any future upload test will need it too.
9. network_use_disclosure is the only .Nillable() optional string in db/schema/mod.go
db/schema/mod.go:43 — every other optional string field uses bare .Optional(). The choice looks intentional (the gating needs *string to distinguish "unset" from "explicitly empty"), but it silently introduces a new convention. Worth a comment on the field. It also creates an asymmetry with ai_use_disclosure (a field.JSON(...).Optional(), not Nillable) that happens to behave the same way only because JSON fields default to nil pointer.
🟡 Style / next time
10. Migration filename outlier
20250411023207_AddNetworkUsageDisclosure.up.sql is the only migration in the repo using PascalCase, and its timestamp is from 2025-04 (a year behind the other three migrations in this PR). Looks carried over from an earlier branch. Renaming now is high-friction (atlas.sum re-hash, anyone with the migration already applied needs a manual fix-up), so feel free to leave — but a next time note for the snake_case + current-date convention.
11. AI + Network disclosure update branches have parallel control flow
gql/resolver_mods.go:195-211 — "if set: validate-and-apply; if cleared while previously set: error" is duplicated across both fields. Could be a SetOmittableNoClear[T] helper next to the existing SetINNF family in gql/gql_utils.go.
✅ Verified non-issues
- New disclosure gating in
CreateVersiondoes not add a DB round-trip — reads off themodrow the resolver already loaded. - No missing indexes — disclosure columns are only read via
Mod.Get(id), never filtered/sorted on. - Windows
.ps1vs Linux.shmise tasks: duplication is acceptable given genuine shell semantics differences. - Controller
unknownstate and Compatibility re-use: correctly threaded throughutil.Compatibility.
|
Addressed 1, 3, 4, 6, 7, 8, 9, 10 Sorta addressed 2Removed the Unspecified enum option, now covered by the field being unset Skipped 5Not sure how to make this work with the Skipped 11Enough purpose built logic where trying to extract would be more work than it's worth |
| [tasks.setup] | ||
| run = [ | ||
| "docker compose --file docker-compose-dev.yml up --detach --wait && sleep 5", | ||
| "mc alias set local http://localhost:9000 minio minio123", | ||
| "mc admin user svcacct remove local/ REPLACE_ME_KEY --dp || echo It is okay for this REMOVE command to fail if the account does not exist", | ||
| "mc admin user svcacct add local minio --access-key REPLACE_ME_KEY --secret-key REPLACE_ME_SECRET", | ||
| "mc anonymous set public local/smr" | ||
| ] | ||
| run_windows = [ | ||
| "docker compose --file docker-compose-dev.yml up --detach --wait && timeout /t 5", | ||
| "mc alias set local http://localhost:9000 minio minio123", | ||
| "mc admin user svcacct remove local/ REPLACE_ME_KEY --dp || echo It is okay for this REMOVE command to fail if the account does not exist", | ||
| "mc admin user svcacct add local minio --access-key REPLACE_ME_KEY --secret-key REPLACE_ME_SECRET", | ||
| "mc anonymous set public local/smr" | ||
| ] | ||
|
|
There was a problem hiding this comment.
Was something broken why this had to be done?
| [tools] | ||
| "aqua:minio/mc" = "RELEASE.2025-04-08T15-39-49Z" | ||
| atlas = "0.32.0" | ||
| atlas = "latest" |
There was a problem hiding this comment.
This should not be pinned to "latest"
| @@ -0,0 +1 @@ | |||
| h1:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= | |||
Backend side of satisfactorymodding/smr-frontend#255
ToggleNetworkUseremoved