test(seed): prove a re-seed leaves five offers at the tuned numbers
The five offer tests this branch added all read the SHOP_OFFERS constant; none ran the seed, so the one guarantee the stable-id design exists to provide -- re-seeding does not duplicate content -- was covered by nothing. This runs `seedVisibleVerticalSlice` twice against an in-memory shop-offer repository and asserts the five stable ids survive. Confirmed catchable: reverting the conflict target to ['shopId', 'itemDefinitionId'] leaves 4 rows under the fake, because both bag offers carry `itemDefinitionId: null` and collapse into one. On Postgres it would fail outright. The prices (12/30/40/35/60) and thresholds (reputation 25 and 40, renown 3) are pinned in the same test: AGENTS.md §39 forbids silent rebalancing, and the structural tests never looked at a number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,10 @@ import { NpcDefinition } from '../../npcs/entities/npc-definition.entity';
|
||||
import { NpcShop } from '../../shops/entities/npc-shop.entity';
|
||||
import { ShopOffer } from '../../shops/entities/shop-offer.entity';
|
||||
import { DEMO_CHARACTER_STARTING_WEAPON_ITEM_ID } from '../../demo/demo-character.constants';
|
||||
import { GameConditionType } from '../../conditions/game-condition.types';
|
||||
import {
|
||||
ComparisonOperator,
|
||||
GameConditionType,
|
||||
} from '../../conditions/game-condition.types';
|
||||
import {
|
||||
ASH_RAT_LOOT_TABLE_ID,
|
||||
CHARRED_LOOTER_LOOT_TABLE_ID,
|
||||
@@ -29,7 +32,7 @@ import {
|
||||
WILD_ROAD_DOG_LOOT_TABLE_ID,
|
||||
} from './item.constants';
|
||||
import { BASIC_HIDE_BAG_ID } from './loot-bag-content';
|
||||
import { SHOP_OFFERS } from './npc-content';
|
||||
import { BORIN_OFFER_IDS, SHOP_OFFERS } from './npc-content';
|
||||
import { seedVisibleVerticalSlice } from './vertical-slice.seed';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
@@ -956,4 +959,98 @@ describe('seedVisibleVerticalSlice', () => {
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
expect(ids.every((id) => id.length === 36)).toBe(true);
|
||||
});
|
||||
|
||||
it('still holds exactly five offers after a re-seed, at the tuned numbers', async () => {
|
||||
const shopOfferRepository = new InMemoryRepository();
|
||||
const dataSource = createDataSource(
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
new InMemoryRepository(),
|
||||
shopOfferRepository,
|
||||
);
|
||||
|
||||
// Twice, because that is the whole point of the stable ids (NPC spec §32).
|
||||
// Both bag offers carry `itemDefinitionId: null`, so the pre-0.8.5 conflict
|
||||
// target of (shopId, itemDefinitionId) would fold them into a single row
|
||||
// here -- and would blow up outright on Postgres, where NULL never equals
|
||||
// NULL in a unique index.
|
||||
await seedVisibleVerticalSlice(dataSource);
|
||||
await seedVisibleVerticalSlice(dataSource);
|
||||
|
||||
expect(shopOfferRepository.rows).toHaveLength(5);
|
||||
expect(
|
||||
shopOfferRepository.rows.map((row: Row) => String(row.id)).sort(),
|
||||
).toEqual(
|
||||
[
|
||||
BORIN_OFFER_IDS.potion,
|
||||
BORIN_OFFER_IDS.shortsword,
|
||||
BORIN_OFFER_IDS.trophyPouch,
|
||||
BORIN_OFFER_IDS.hideBag,
|
||||
BORIN_OFFER_IDS.banditBlade,
|
||||
].sort(),
|
||||
);
|
||||
|
||||
// Prices and thresholds are balancing decisions, not incidentals, so a
|
||||
// refactor must not be able to drift one silently (AGENTS.md §39).
|
||||
const byId = new Map<string, Row>(
|
||||
shopOfferRepository.rows.map((row: Row): [string, Row] => [
|
||||
String(row.id),
|
||||
row,
|
||||
]),
|
||||
);
|
||||
expect(byId.get(BORIN_OFFER_IDS.potion)).toMatchObject({
|
||||
price: 12,
|
||||
conditions: [],
|
||||
});
|
||||
expect(byId.get(BORIN_OFFER_IDS.shortsword)).toMatchObject({
|
||||
price: 30,
|
||||
conditions: [],
|
||||
});
|
||||
expect(byId.get(BORIN_OFFER_IDS.trophyPouch)).toMatchObject({
|
||||
price: 40,
|
||||
conditions: [
|
||||
{
|
||||
type: GameConditionType.REGION_REPUTATION,
|
||||
key: 'border-guard',
|
||||
operator: ComparisonOperator.GTE,
|
||||
value: 25,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(byId.get(BORIN_OFFER_IDS.hideBag)).toMatchObject({
|
||||
price: 35,
|
||||
conditions: [
|
||||
{
|
||||
type: GameConditionType.REGION_REPUTATION,
|
||||
key: 'border-guard',
|
||||
operator: ComparisonOperator.GTE,
|
||||
value: 40,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(byId.get(BORIN_OFFER_IDS.banditBlade)).toMatchObject({
|
||||
price: 60,
|
||||
conditions: [
|
||||
{
|
||||
type: GameConditionType.WORLD_RENOWN,
|
||||
operator: ComparisonOperator.GTE,
|
||||
value: 3,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user