Files
Its-Achievable/tests/PLAN.md
Kaysser Kayyali 888790e5ff v0.1.1: track the hax-hooks-lib -> foundry-hooks-lib rename
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.
2026-06-20 16:55:29 -04:00

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 — numeric
  • in / notIn — array membership
  • contains — 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 to game.settings.get("its-achievable", "achievementsByActor")[actorKey].
  • getActorAchievements(actorKey) reads back what awardAchievement wrote.
  • 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:

  • subscribeMany is 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.md D4.
  • If foundry-hooks-lib is not installed, its-achievable logs a warning and continues (graceful degradation).
  • If foundry-hooks-lib is installed but battle-focus is not, the HUD's getActiveEncounter() 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 with name, tokenId, portrait)
  • combatants (array; each has tokenId, 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.css is hand-verified visually during live development, not in the smoke test.
  • Settings migration from battle-focus.* to its-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:

  1. 100% of the "What we test" bullets have an assertion.
  2. npm test exits 0 in the no-Foundry smoke runner. Runs in <2s.
  3. npm run lint (if added) exits 0; otherwise skipped.
  4. 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.mjs Playwright driver will be added when there's a real consumer driving the integration.