test(shop): make three nominal spec §10 cases exercise what they claim

Case 7 ("price is still required even when the reputation condition is met")
and case 3 ("sufficient regional reputation allows purchase") both built
fixtures with `conditions: []`. With no requirement present, none can be met,
so neither test touched the gate it was named after. Both now carry a
satisfied REGION_REPUTATION condition.

Case 4 ("insufficient World Renown blocks purchase") had no test at all. It
matters because a renown block must surface as SHOP_OFFER_LOCKED rather than
MERCHANT_REPUTATION_TOO_LOW -- renown is not the merchant's regard, and
telling the player to go and earn reputation would point at the wrong bar.
Verified by widening the reputation-blame check to include WORLD_RENOWN, which
fails the new test alone.

Also pins a bag offer's description to empty, so the duplicate capacity line
cannot come back through the API side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-22 21:07:25 +02:00
parent f0edf89ad3
commit a454cf955e

View File

@@ -491,10 +491,27 @@ describe('ShopService', () => {
effectSummary: 'Capacity: 5 Raider Trophies', effectSummary: 'Capacity: 5 Raider Trophies',
unlocked: true, unlocked: true,
}); });
// The capacity line is sent once, as the effect. Repeating it as the
// description makes the row render it twice (slice §5).
expect(view.offers[0].itemDescription).toBe('');
}); });
it('grants a bag rather than stacking it as an item', async () => { it('grants a bag rather than stacking it as an item', async () => {
const world = createWorld({ bagOffer: true, silver: 100 }); // Slice §10 case 3: the reputation gate is present *and* satisfied, so the
// purchase goes through. A fixture with no conditions at all would pass
// without the gate ever being consulted.
const world = createWorld({
bagOffer: true,
silver: 100,
conditions: [
{
type: GameConditionType.REGION_REPUTATION,
key: 'border-guard',
operator: ComparisonOperator.GTE,
value: 25,
},
],
});
const result = await world.service.purchase( const result = await world.service.purchase(
CHARACTER_ID, CHARACTER_ID,
@@ -615,6 +632,36 @@ describe('ShopService', () => {
expect(world.grantedItems).toHaveLength(0); expect(world.grantedItems).toHaveLength(0);
}); });
it('blocks a renown-gated offer without blaming merchant reputation', async () => {
// Slice §10 case 4, and the discrimination this branch introduced: World
// Renown is not the merchant's regard, so a renown block must read as a
// plain lock. Sending this player off to trade pelts with Borin would be
// pointing at the wrong bar entirely.
const world = createWorld({
silver: 1000,
unmetConditionTypes: [GameConditionType.WORLD_RENOWN],
conditions: [
{
type: GameConditionType.WORLD_RENOWN,
operator: ComparisonOperator.GTE,
value: 3,
},
],
});
await expect(
world.service.purchase(
CHARACTER_ID,
MERCHANT_KEY,
'small-healing-potion',
1,
),
).rejects.toMatchObject({ code: 'SHOP_OFFER_LOCKED' });
expect(world.character.silver).toBe(1000);
expect(world.grantedItems).toHaveLength(0);
});
it('opens an offer whose bypass holds even though its conditions do not', async () => { it('opens an offer whose bypass holds even though its conditions do not', async () => {
// The Slice 0.9 referral: the warden's word is worth more than the // The Slice 0.9 referral: the warden's word is worth more than the
// reputation the player has not earned yet (slice §7). // reputation the player has not earned yet (slice §7).
@@ -653,8 +700,21 @@ describe('ShopService', () => {
}); });
it('still charges the price when a requirement is met', async () => { it('still charges the price when a requirement is met', async () => {
// Reputation opens the offer; it does not pay for it (slice §10). // Reputation opens the offer; it does not pay for it (slice §10). The
const world = createWorld({ bagOffer: true, silver: 10 }); // requirement has to actually exist and hold for that to be the thing
// under test.
const world = createWorld({
bagOffer: true,
silver: 10,
conditions: [
{
type: GameConditionType.REGION_REPUTATION,
key: 'border-guard',
operator: ComparisonOperator.GTE,
value: 25,
},
],
});
await expect( await expect(
world.service.purchase( world.service.purchase(