User callout: 'Hax' is Kaysser's nickname, drop it from module names. hooks-lib v0.3.0 renamed its module id; this commit follows: - relationships.modules[0].id: hax-hooks-lib -> foundry-hooks-lib - relationships.modules[0].compatibility.minimum: 0.2.0 -> 0.3.0 (we now require the renamed version) - module.json: version 0.1.0 -> 0.1.1, download URL updated - README.md, scripts/main.js, tests/PLAN.md, tests/test-helpers.mjs: branding text updates - .gitignore: un-ignore the new 0.1.1 zip Verification: 75/75 smoke assertions pass, no logic change. Module title is now 'It's Achievable' (dropped the 'Hax's Tools' umbrella prefix per the same callout). 2 module text changes are non-functional; pure branding.
5.4 KiB
its-achievable test plan — v0.1.0
Status: Implements v0.1.0. The plan mirrors the structure of
hooks-lib/tests/PLAN.md (sections A-F) but is scoped to
its-achievable-specific behavior.
Drives: tests/verify-achievable-v1.mjs (no-Foundry smoke test).
What we test (must pass for "done")
Section A — Rule engine unit tests
achievement-rules.js is pure data + functions; no Foundry needed.
Operators (8):
equals/notEquals— strict equality (===/!==)gt/gte/lt/lte— numericin/notIn— array membershipcontains— substring (string) OR has-property (object)exists/notExists— field present (or not) in object
For each operator: at least one positive and one negative assertion.
Rule evaluation:
evaluateRulesForEvent(event, encounter, actor, targetActor)matches rules whose trigger conditions are all true.- Rules with multiple conditions: ALL must match (AND semantics).
- Rules with no conditions: never fire (defensive default).
- Rules with bad field paths: silently skip that condition (don't throw, but log a debug message).
Trigger types:
event— fire when conditions match the event context.encounter-end— fire at combat-end with the encounter stats.career-update— fire when the PC's career is updated.
Section B — Achievement catalog
getAchievementCatalog() returns the built-in catalog (24 entries
per slice 8 of battle-focus) plus any user-defined custom rules.
Test asserts: catalog is an array, length ≥ 24, every entry has
{id, name, description, icon, tier, trigger}.
Section C — Award + lookup
awardAchievement(actorKey, achievementId, encounterId)writes togame.settings.get("its-achievable", "achievementsByActor")[actorKey].getActorAchievements(actorKey)reads back whatawardAchievementwrote.- Idempotency: awarding the same achievement twice does not duplicate.
Section D — Hooks-lib subscription wiring
its-achievable's ready hook subscribes to hooks-lib's envelope stream.
Smoke test stubs both Hooks (for Foundry init/ready) and
foundry-hooks-lib's mod.api (for subscribe) and asserts:
subscribeManyis called with at least the combat + actor update + token update + dnd5e roll hooks listed in.hermes/plans/2026-06-20_080000-hax-tools-stage2-achievable-v2.mdD4.- If
foundry-hooks-libis not installed, its-achievable logs a warning and continues (graceful degradation). - If
foundry-hooks-libis installed butbattle-focusis not, the HUD'sgetActiveEncounter()returns null and the HUD skips rendering.
Section E — HUD payload derivation
buildHudUpdatePayload(encounter, event) produces the same shape
battle-focus's existing implementation produces (verified against
battle-focus's source — see battle-focus/scripts/main.js lines
560-600). Test asserts the payload contains:
round,turn,currentTurn(object withname,tokenId,portrait)combatants(array; each hastokenId,actorId,name,isPlayer,side,status,damageDealt,damageTaken,hits,crits,portrait,hpPct)startedAt
(The raw event field is not in the payload — Kaysser confirmed
to trim that in a future pass; for now it's still in there because
the HUD's dice-streak extraction needs it. Documented as a TODO in
the code.)
Section F — Wall + popover rendering
renderAchievementWall(actorId, actorName, opts) returns HTML
containing the actor's name and any earned achievements.
getAchievementWallProgress(actorId, actorName) returns progress
tuples for un-earned milestones.
renderAchievementPopover(unlocks, viewerName) returns HTML for the
chat-bar popover.
What we don't test (explicitly out of scope)
- Real Foundry runtime. The smoke test stubs Foundry. Live integration testing happens when battle-focus migrates in Stage 3 and its E2E exercises the moved code.
- Custom-achievements FormApplication rendering. The FormApplication
class is Foundry-specific (extends FormApplication); testing it
requires a real Foundry. The form's logic (validate, save, test) IS
tested in the smoke test via direct calls to
validateRule,testRule,getCustomRules,setCustomRules. - CSS rendering.
styles/hud.cssis hand-verified visually during live development, not in the smoke test. - Settings migration from
battle-focus.*toits-achievable.*. Per Kaysser's decision, no migration. Users with existing worlds re-create their rules.
Definition of done
A v0.1.0 release is "done" when:
- 100% of the "What we test" bullets have an assertion.
npm testexits 0 in the no-Foundry smoke runner. Runs in <2s.npm run lint(if added) exits 0; otherwise skipped.- Both pass on the Hermes shell (Windows, git-bash, Node 18+).
Test files (v0.1.0)
| File | Purpose |
|---|---|
tests/verify-achievable-v1.mjs |
No-Foundry smoke. Sections A-F. Runs in <2s. |
tests/test-helpers.mjs |
Foundry stub (Hooks, game, ui, mod.api). |
Future turns (when this repo is no longer the focus)
- When battle-focus migrates (Stage 3), the battle-focus test driver will exercise its-achievable through real Foundry. battle-focus's test plan will reference its-achievable's test plan for unit assertions and add Foundry-specific integration assertions.
- A
tests/verify-achievable-foundry.mjsPlaywright driver will be added when there's a real consumer driving the integration.