Skip to content

fix(character): insert character row before provisioning to satisfy inventory FK#55

Merged
Mnehmos merged 2 commits intomainfrom
fix/45-equipment-template-seed
Apr 20, 2026
Merged

fix(character): insert character row before provisioning to satisfy inventory FK#55
Mnehmos merged 2 commits intomainfrom
fix/45-equipment-template-seed

Conversation

@Mnehmos
Copy link
Copy Markdown
Owner

@Mnehmos Mnehmos commented Apr 18, 2026

Summary

handleCreate built the provisioning result, then assembled the character object, then inserted the character row. The provisioner writes to inventory_items immediately, and that table FK-references characters(id). Result: every starting-equipment grant failed with FOREIGN KEY constraint failed, every PC spawned with an empty bag.

Fix

Reorder to insert the character row first, then run provisioning, then update the character with the slot/spell data the provisioner returns. (The provisioner is read-only for slot/spell data; only inventory + currency need a writable character row.)

Test plan

  • Regression test creates one character per Paladin/Rogue/Wizard/Cleric/Fighter at L4; asserts _provisioning.errors is undefined and equipmentGranted.length > 0.
  • character-manage suite: 39/39 pass.
  • Full suite: 1890 passed, 6 skipped, 0 failed.

Closes #45

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed character creation so starting equipment and spell-related data (known spells, cantrips, spell-slot and pact-magic values) are correctly initialized, derived, and persisted; spell slot levels now map correctly from provisioning data.
  • Tests

    • Added regression tests to verify equipment provisioning succeeds for multiple classes and that spell slot maxima are correct after creation.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

Reordered character creation to persist the character row before running starting-equipment provisioning; spell-related fields are derived after provisioning (with zero-indexed spell-slot array → level mapping) and persisted via an update. Added regression tests checking provisioning succeeds and spell-slot indexing is correct.

Changes

Cohort / File(s) Summary
Character creation & spell-slot logic
src/server/consolidated/character-manage.ts
Create character row first, call provisionStartingEquipment(), then derive/update spell fields. Updated convertSpellSlotsToObject() to map zero-indexed arrays (slots[0]→level1, slots[1]→level2, ...).
Provisioning & indexing tests
tests/server/consolidated/character-manage.test.ts
Added create-action regression tests: one iterates classes to assert _provisioning exists, no _provisioning.errors, and equipment granted; another verifies spell slot maxima for Wizard/Paladin to confirm zero-indexed slot mapping.

Sequence Diagram

sequenceDiagram
    actor Client
    participant Handler as Character Handler
    participant CharRepo as Character Repository
    participant ProvService as Provisioning Service
    participant DB as Database

    Client->>Handler: create(class, level, provisionEquipment)
    Handler->>Handler: Build character object (seed spells from args only)
    
    rect rgba(100,150,255,0.5)
    Note over Handler,CharRepo: Insert character first
    Handler->>CharRepo: create(character)
    CharRepo->>DB: INSERT character
    DB-->>CharRepo: character.id
    CharRepo-->>Handler: character (with id)
    end

    rect rgba(150,200,100,0.5)
    Note over Handler,ProvService: Provision starting equipment & spells
    Handler->>ProvService: provisionStartingEquipment(character.id, ...)
    ProvService->>DB: INSERT inventory rows (FK to character.id)
    DB-->>ProvService: provisioning result (equipmentGranted, spellsGranted, spellSlots)
    ProvService-->>Handler: provisioning result
    end

    rect rgba(200,150,100,0.5)
    Note over Handler,CharRepo: Derive spell fields and persist
    Handler->>Handler: Merge knownSpells (args ∪ spellsGranted), set cantrips, convert zero-indexed spellSlots
    Handler->>CharRepo: update(character.id, { knownSpells, cantripsKnown, spellSlots, pactMagicSlots })
    CharRepo->>DB: UPDATE character
    DB-->>CharRepo: success
    CharRepo-->>Handler: updated character
    end

    Handler-->>Client: character + _provisioning details
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

Poem

🐰 I planted a row, then gave a sword and bow,
Slots now count from zero, spells neatly in a row.
I hopped through tests and fixed the bake,
No FK crumbs left in the inventory cake.
Hooray—new characters ready to go! 🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and clearly describes the main change: reordering the character row insertion before provisioning to satisfy foreign key constraints.
Linked Issues check ✅ Passed The PR fulfills issue #45 requirements: character row is inserted before provisioning, the provisioning write succeeds with inventory FKs, and tests verify zero errors and non-empty equipment grants.
Out of Scope Changes check ✅ Passed All changes directly address issue #45: character insertion order, spell slot indexing fix for correctness post-reordering, and regression tests validating the fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/45-equipment-template-seed

Comment @coderabbitai help to get the list of available commands and usage tips.

…nventory FK

handleCreate built the provisioning result first, then assembled the
character record, then inserted it. The provisioner writes to
inventory_items immediately, and that table's character_id column has
a FOREIGN KEY → characters(id). Since the character row didn't exist
yet, every inventory insert failed and surfaced as
"Failed to grant <Item>: FOREIGN KEY constraint failed" — every PC
spawned with an empty inventory.

Fix:
- Insert the character row first (with whatever args supplied for spell
  fields, defaulted otherwise).
- Run provisioning second — FK now resolves, inventory grants succeed.
- Update the character row with knownSpells/cantripsKnown/spellSlots/
  pactMagicSlots from the provisioning result.

Adds a regression test that creates one character per major class and
asserts _provisioning.errors stays unset and equipmentGranted is non-empty.

Closes #45

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/server/consolidated/character-manage.test.ts (1)

102-115: Assert persisted inventory state, not just _provisioning.

This regression still only checks the handler response. Since issue #45 was specifically a failed write to inventory_items, please also verify the created character has persisted inventory rows after creation—ideally by querying through InventoryRepository or the DB and checking for expected starter items per class.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/server/consolidated/character-manage.test.ts` around lines 102 - 115,
The test currently only asserts the handler response (_provisioning) but must
also assert persisted inventory rows to catch failed writes to inventory_items;
after each handleCharacterManage call, load the created character's inventory
via InventoryRepository (or a direct DB query) using the returned character id
from parsed, then assert that InventoryRepository.findByCharacterId (or
equivalent) returns >0 rows and contains the expected starter item(s) for that
class (e.g., Paladin, Rogue, Wizard, Cleric, Fighter); update the test to fail
if no persisted inventory rows exist or expected starter items are missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/server/consolidated/character-manage.ts`:
- Around line 239-245: The code is shifting spell-slot levels when serializing
provisioningResult.spellSlots via convertSpellSlotsToObject; normalize the array
shape before calling convertSpellSlotsToObject so slot indexes align with the
helper's expected indexing (handle both 0-based and 1-based shapes produced by
provisionStartingEquipment). Update the call site that sets character.spellSlots
to detect provisioningResult.spellSlots and, if its first element corresponds to
1st-level slots (the starting-equipment shape), prepend a placeholder (e.g.,
null/0) or otherwise shift the array so convertSpellSlotsToObject reads the
correct level positions; alternatively, adjust convertSpellSlotsToObject to
accept and normalize both shapes—refer to provisioningResult.spellSlots and
convertSpellSlotsToObject to locate the change.

---

Nitpick comments:
In `@tests/server/consolidated/character-manage.test.ts`:
- Around line 102-115: The test currently only asserts the handler response
(_provisioning) but must also assert persisted inventory rows to catch failed
writes to inventory_items; after each handleCharacterManage call, load the
created character's inventory via InventoryRepository (or a direct DB query)
using the returned character id from parsed, then assert that
InventoryRepository.findByCharacterId (or equivalent) returns >0 rows and
contains the expected starter item(s) for that class (e.g., Paladin, Rogue,
Wizard, Cleric, Fighter); update the test to fail if no persisted inventory rows
exist or expected starter items are missing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a73ef1c7-a6fb-4926-b850-7698b0cff6ca

📥 Commits

Reviewing files that changed from the base of the PR and between 2d9ddb9 and baf6d02.

📒 Files selected for processing (2)
  • src/server/consolidated/character-manage.ts
  • tests/server/consolidated/character-manage.test.ts

Comment thread src/server/consolidated/character-manage.ts
…lot bug

Reviewers (PR #55) flagged that this branch reorders character creation
so provisioning succeeds, but it still uses the off-by-one
convertSpellSlotsToObject helper. Merged standalone, every wizard would
persist with level1=3 (should be 4) and every paladin with level1=0
(should be 3).

Apply the same zero-indexing fix that lives in PR #54 here so #55 is
correct on its own merge boundary. Tests assert wizard L4 = 4/3 and
paladin L4 = 3 after the reorder.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/server/consolidated/character-manage.ts (1)

224-257: Post-provisioning update correctly persists spell-related fields.

The flow is sound:

  1. Provisioning runs against the existing character row
  2. Spell fields are derived from provisioning result and merged with args
  3. A single update() call persists all spell-related changes

One minor observation: the as any cast on line 256 bypasses type checking on the update payload.

🔧 Consider defining the update payload type explicitly

Instead of as any, you could define a typed partial:

-        characterRepo.update(characterId, {
-            knownSpells: character.knownSpells as string[],
-            cantripsKnown: character.cantripsKnown as string[],
-            spellSlots: character.spellSlots,
-            pactMagicSlots: character.pactMagicSlots
-        } as any);
+        const spellUpdates: Partial<{
+            knownSpells: string[];
+            cantripsKnown: string[];
+            spellSlots: ReturnType<typeof convertSpellSlotsToObject>;
+            pactMagicSlots: typeof character.pactMagicSlots;
+        }> = {
+            knownSpells: character.knownSpells as string[],
+            cantripsKnown: character.cantripsKnown as string[],
+            spellSlots: character.spellSlots,
+            pactMagicSlots: character.pactMagicSlots
+        };
+        characterRepo.update(characterId, spellUpdates as Parameters<typeof characterRepo.update>[1]);

This is a minor improvement and can be deferred.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/consolidated/character-manage.ts` around lines 224 - 257, The
update call to characterRepo.update uses an unsafe cast ("as any") for the
payload; replace it with a properly typed partial object so TypeScript enforces
field types. Locate the provisioning block around
provisionStartingEquipment(...) and the subsequent assignments to
character.knownSpells, character.cantripsKnown, character.spellSlots, and
character.pactMagicSlots, then call characterRepo.update(characterId, payload)
where payload is typed (e.g., Partial<CharacterRow> or the repository's expected
update type) and contains knownSpells: string[], cantripsKnown: string[],
spellSlots: SpellSlotsType, pactMagicSlots?: PactMagicType; remove the "as any"
cast and adjust conversions (convertSpellSlotsToObject return type) to satisfy
the update signature.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/server/consolidated/character-manage.ts`:
- Around line 224-257: The update call to characterRepo.update uses an unsafe
cast ("as any") for the payload; replace it with a properly typed partial object
so TypeScript enforces field types. Locate the provisioning block around
provisionStartingEquipment(...) and the subsequent assignments to
character.knownSpells, character.cantripsKnown, character.spellSlots, and
character.pactMagicSlots, then call characterRepo.update(characterId, payload)
where payload is typed (e.g., Partial<CharacterRow> or the repository's expected
update type) and contains knownSpells: string[], cantripsKnown: string[],
spellSlots: SpellSlotsType, pactMagicSlots?: PactMagicType; remove the "as any"
cast and adjust conversions (convertSpellSlotsToObject return type) to satisfy
the update signature.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 12d6df03-94c9-4ff4-87a2-90263d71d431

📥 Commits

Reviewing files that changed from the base of the PR and between baf6d02 and 2da8f4d.

📒 Files selected for processing (2)
  • src/server/consolidated/character-manage.ts
  • tests/server/consolidated/character-manage.test.ts

@Mnehmos
Copy link
Copy Markdown
Owner Author

Mnehmos commented Apr 19, 2026

@coderabbitai this finding is stale — the fix landed in 2da8f4d (this branch's tip). convertSpellSlotsToObject now uses slots[0] for level1 (see character-manage.ts:163) and the regression test asserts wizard L4 = 4/3 / paladin L4 = 3/0/0.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

Tip

For best results, initiate chat on the files or code changes.

@Mnehmos You're right — the AI-generated summary confirms the zero-indexed fix is already in place: slots[0]level1, slots[1]level2, etc. Apologies for the stale finding.

@Mnehmos Mnehmos merged commit 45d8c5d into main Apr 20, 2026
4 checks passed
@Mnehmos Mnehmos deleted the fix/45-equipment-template-seed branch April 20, 2026 14:24
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.

Equipment provisioning fails with FOREIGN KEY constraint for every starting item

1 participant