Files
Dune/tests/reference.test.ts
T

410 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import {
RULE_TOPICS,
RULE_CHECKLIST_AREAS,
PHASE_HELP,
phaseRuleId,
} from '../game/reference';
import { FACTIONS, PHASES } from '../game/catalog';
import { readFileSync } from 'node:fs';
void test('every phase and base faction has a discoverable rule topic with valid related links', () => {
const ids = new Set(RULE_TOPICS.map((t) => t.id));
assert.equal(ids.size, RULE_TOPICS.length);
for (const topic of RULE_TOPICS) {
assert.ok(topic.steps.length > 0, topic.id);
for (const related of topic.related ?? [])
assert.ok(ids.has(related), `${topic.id} -> ${related}`);
}
assert.equal(PHASE_HELP.length, PHASES.length);
PHASES.forEach((_, i) => assert.ok(ids.has(phaseRuleId(i))));
FACTIONS.forEach((f) => assert.ok(ids.has(`faction-${f.id}`)));
assert.ok(ids.has('ai-players'));
});
void test('reviewed feature checklists expose all five independent facets and real local verification evidence', () => {
const hub = RULE_TOPICS.find(
(topic) => topic.id === 'implementation-checklist',
)!;
const features = [
'setup',
'truthtrance-spice',
'richese-cards',
'richese-acquisition',
'richese-no-field',
'nullentropy-search',
'ecaz-ambassadors',
'automatic-casualties',
'automatic-responses',
'automatic-decisions',
'ai-pacing',
];
for (const id of features) {
const topic = RULE_TOPICS.find((candidate) => candidate.id === id)!;
assert.ok(
hub.related?.includes(id),
`${id} is discoverable from the checklist hub`,
);
assert.deepEqual(
topic.checklist?.map((item) => item.area),
[...RULE_CHECKLIST_AREAS],
id,
);
assert.equal(
topic.checklist?.find((item) => item.area === 'Verification')?.status,
'Partial',
'focused tests do not certify all browser and expansion interactions',
);
for (const item of topic.checklist ?? []) {
assert.ok(
item.detail.length > 20,
`${id}: ${item.area} has an explicit scope`,
);
for (const evidence of item.evidence ?? []) {
assert.match(evidence, /^tests\/[a-z0-9-]+\.test\.tsx?$/);
assert.match(
readFileSync(new URL(`../${evidence}`, import.meta.url), 'utf8'),
/test\(/,
evidence,
);
}
}
}
});
void test('Richese checklist reports integrated auctions separately from inert card effects and unresolved rules', () => {
const topic = RULE_TOPICS.find(
(candidate) => candidate.id === 'richese-cards',
)!;
const guide = topic.steps.join(' ');
assert.equal(topic.coverage, 'Partial');
for (const phrase of [
'Once Around or Silent bidding',
'normal, Once Around or Silent bidding',
'Silent bids remain private',
'pledged ally spice',
'Atreides inspection',
'Harkonnen bonus',
'positive Black Market self-bids',
'exhausted-cache arithmetic',
'Ixian Technology substitution',
'skips the optional Black Market prelude',
'Juice of Sapho',
'Richese expansion starts remain disabled',
'persisted-room checks pass',
])
assert.ok(guide.includes(phrase), phrase);
assert.match(
guide,
/implementation guards.*not claims of official prohibitions/,
);
assert.equal(
topic.checklist!.find((item) => item.area === 'AI')!.status,
'Partial',
);
assert.ok(
topic
.checklist!.find((item) => item.area === 'Verification')!
.evidence!.includes('tests/richese-auction-bots.test.ts'),
);
assert.ok(
topic
.checklist!.find((item) => item.area === 'Verification')!
.evidence!.includes('tests/richese-engine.test.ts'),
);
assert.doesNotMatch(JSON.stringify(topic), /https?:\/\//);
});
void test('Richese acquisition guide separates purchase immunity, income response and independent transfer/search controls', () => {
const topic = RULE_TOPICS.find(
(candidate) => candidate.id === 'richese-acquisition',
)!;
assert.equal(topic.coverage, 'Partial');
const text = topic.steps.join(' ');
for (const phrase of [
'once per game',
'enters your hand secretly',
'cannot cancel the special purchase',
'Emperor income has a separate response',
'Full-hand activation',
'final cached card',
'have their own hand-panel controls',
])
assert.ok(text.includes(phrase), phrase);
assert.equal(topic.checklist!.length, 5);
assert.ok(topic.checklist!.every((item) => item.status === 'Partial'));
assert.ok(
topic
.checklist!.find((item) => item.area === 'Verification')!
.evidence!.includes('tests/richese-acquisition-bots.test.ts'),
);
assert.doesNotMatch(JSON.stringify(topic), /https?:\/\//);
});
void test('Ecaz checklist distinguishes integrated lifecycle from unopened expansion starts', () => {
const topic = RULE_TOPICS.find(
(candidate) => candidate.id === 'ecaz-ambassadors',
)!;
assert.equal(topic.coverage, 'Partial');
const guide = topic.steps.join(' ');
assert.match(guide, /before spice is paid/);
assert.match(guide, /previous placements and payments remain/);
assert.match(
guide,
/supply policy is an interpretation, not an explicit official FAQ ruling/,
);
assert.match(
guide,
/CHOAM market, deferred technology income, then Ecaz placement/,
);
assert.match(guide, /not a priority established by a combined official FAQ/);
assert.match(
guide,
/Storm crossings return tokens.*overriding traitor victory leaves it placed/,
);
assert.match(guide, /Full Ecaz starts remain disabled/);
assert.ok(
RULE_TOPICS.find(
(candidate) => candidate.id === 'revival',
)!.related?.includes(topic.id),
);
assert.ok(
RULE_TOPICS.find(
(candidate) => candidate.id === 'ecaz-modules',
)!.related?.includes(topic.id),
);
});
void test('automation reference keeps single-outcome settlement, cosmetic notices and paced AI distinct', () => {
const casualty = RULE_TOPICS.find(
(topic) => topic.id === 'automatic-casualties',
)!;
const pacing = RULE_TOPICS.find((topic) => topic.id === 'ai-pacing')!;
assert.match(
casualty.steps.join(' '),
/If several physical casualty combinations are legal, the winner still chooses/,
);
assert.match(casualty.steps.join(' '), /1\.5 seconds each/);
assert.match(casualty.steps.join(' '), /while notices are off are consumed/);
assert.match(casualty.steps.join(' '), /turning notices on does not replay/);
assert.match(
casualty.steps.join(' '),
/Successful powers without a notice tag remain outside/,
);
assert.match(
casualty.steps.join(' '),
/hiding notices does not disable automatic rule settlement or change AI timing/,
);
assert.match(
casualty.steps.join(' '),
/Uncancelable responses now settle automatically/,
);
for (const event of [
'successful Ecaz Ambassador placement',
'Emperor auction income',
'Harkonnen bonus-card draws',
'private full-plan inspection availability',
])
assert.ok(casualty.steps.join(' ').includes(event), event);
assert.match(
casualty.steps.join(' '),
/without exposing its private card identity/,
);
assert.match(pacing.steps.join(' '), /at most one AI action per step/);
assert.match(pacing.steps.join(' '), /Competing workers cannot both commit/);
assert.match(pacing.steps.join(' '), /Offline simulations use fast batches/);
assert.ok(
RULE_TOPICS.find((topic) => topic.id === 'battle')!.related?.includes(
casualty.id,
),
);
assert.ok(
RULE_TOPICS.find((topic) => topic.id === 'ai-players')!.related?.includes(
pacing.id,
),
);
});
void test('player-facing entry and rules pages contain no external navigation links', () => {
for (const file of [
'app/page.tsx',
'components/game-table.tsx',
'components/rules-reference.tsx',
'app/rules/coverage/page.tsx',
]) {
const source = readFileSync(new URL(`../${file}`, import.meta.url), 'utf8');
assert.doesNotMatch(source, /href\s*=\s*["'](?:https?:)?\/\//, file);
}
});
void test('automatic allowance checklist distinguishes implemented physical custody from conservative promise legality and incomplete notice coverage', () => {
const topic = RULE_TOPICS.find(
(candidate) => candidate.id === 'automatic-responses',
)!;
const guide = topic.steps.join(' ');
assert.equal(topic.coverage, 'Partial');
assert.match(
guide,
/owner cannot cancel that power and needs no Allow click/,
);
assert.match(guide, /only your own cancelable cards/);
assert.match(guide, /Other players’ pass choices.*are not exposed/);
assert.match(guide, /already allowed may still cancel/);
assert.match(guide, /Allies retain legal cancellation choices/);
assert.match(
guide,
/active Truthtrance question or phase-opening choice pauses/,
);
assert.match(
guide,
/remain listed conservatively.*would break a promise and be rejected/,
);
assert.match(
guide,
/merely preparing a player’s view does not award spice or draw cards/,
);
assert.match(guide, /all successful-power notice tags.*remain unfinished/);
const verification = topic.checklist!.find(
(item) => item.area === 'Verification',
)!;
assert.equal(verification.status, 'Partial');
assert.ok(
verification.evidence?.includes('tests/automatic-responses.test.ts'),
);
assert.ok(
verification.evidence?.includes(
'tests/automatic-response-recovery.test.ts',
),
);
assert.equal(
topic.checklist!.find((item) => item.area === 'Player controls')!.status,
'Partial',
);
assert.ok(topic.related?.includes('card-truthtrance'));
assert.ok(
RULE_TOPICS.find(
(candidate) => candidate.id === 'automatic-casualties',
)!.related?.includes(topic.id),
);
});
void test('no-choice decision checklist preserves genuine alternatives and persistent owner-only inspection', () => {
const topic = RULE_TOPICS.find(
(candidate) => candidate.id === 'automatic-decisions',
)!;
const guide = topic.steps.join(' ');
assert.equal(topic.coverage, 'Partial');
assert.match(guide, /declared split.*can be paid exactly/);
assert.match(
guide,
/genuine Karama option or an unfunded payment still requires/,
);
assert.match(guide, /There is no Finish inspection action/);
assert.match(guide, /outside the battle helping an ally/);
assert.match(guide, /not copied into the ally’s private view/);
assert.match(
guide,
/Card enlargement and Truthtrance promise controls remain separate/,
);
assert.match(
guide,
/Other decisions with only one legal outcome.*remain under review/,
);
const special = RULE_TOPICS.find(
(candidate) => candidate.id === 'special-karama',
)!;
assert.match(
special.steps.join(' '),
/No inspection acknowledgement is required/,
);
assert.doesNotMatch(
special.steps.join(' '),
/table pauses while Atreides reads/,
);
assert.ok(
topic
.checklist!.find((item) => item.area === 'Verification')!
.evidence!.includes('tests/automatic-decisions.test.ts'),
);
});
void test('Ecaz entry checklist separates eight supported effects and private historical snapshots from unopened interactions', () => {
const topic = RULE_TOPICS.find(
(candidate) => candidate.id === 'ecaz-ambassadors',
)!;
const guide = topic.steps.join(' ');
assert.equal(topic.coverage, 'Partial');
assert.match(
guide,
/Emperor, Atreides, Harkonnen, CHOAM, Ixian, Richese, Fremen and Guild entry effects are integrated/,
);
assert.match(guide, /Bene Gesserit copies of those eight effects/);
assert.match(guide, /recipient’s private inspection history/);
assert.match(guide, /do not become a live view of later changes/);
assert.match(guide, /closing them needs no game acknowledgement/);
assert.match(guide, /including none, for three spice each/);
assert.match(guide, /Ixians require one available card/);
assert.match(
guide,
/Entries with competing arrival reactions remain blocked/,
);
assert.match(guide, /Full Ecaz starts remain disabled/);
const evidence = topic.checklist!.find(
(item) => item.area === 'Verification',
)!.evidence!;
assert.ok(evidence.includes('tests/ecaz-entry.test.ts'));
assert.ok(evidence.includes('tests/ecaz-entry-bots.test.ts'));
assert.ok(evidence.includes('tests/guild-ambassador-bots.test.ts'));
assert.ok(evidence.includes('tests/guild-ambassador-engine.test.ts'));
assert.match(
guide,
/No-Field substitution.*awaits the user’s interpretation/,
);
assert.match(
guide,
/Simultaneous Intrusion\/Terror and Ambassador\/Terror combinations remain blocked/,
);
});
void test('Nullentropy checklist preserves paid-only privacy, real choices, recovery and explicit interpretation guards', () => {
const topic = RULE_TOPICS.find(
(candidate) => candidate.id === 'nullentropy-search',
)!;
const text = topic.steps.join(' ');
assert.equal(topic.coverage, 'Partial');
assert.equal(topic.checklist!.length, 5);
assert.ok(topic.checklist!.every((item) => item.status === 'Partial'));
for (const phrase of [
'two spice to the bank',
'no discard names or candidate count',
'Only the paying player',
'no free cancellation',
'exactly one legal card',
'without charging again',
'Temporary search access ends',
'pre-existing free hand slot',
'pending Guild refund claim',
'not a general ban',
'before payment',
'without discard lookahead',
'full Richese starts remain disabled',
]) {
assert.ok(
(text + JSON.stringify(topic.checklist)).includes(phrase),
phrase,
);
}
assert.ok(
topic
.checklist!.find((item) => item.area === 'Verification')!
.evidence!.includes('tests/nullentropy-box-recovery.test.ts'),
);
assert.ok(
RULE_TOPICS.find(
(candidate) => candidate.id === 'card-richese-nullentropy-box',
)!.related!.includes(topic.id),
);
assert.doesNotMatch(JSON.stringify(topic), /https?:\/\//);
});