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:
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user