Skip to main content

Code frameworks

Testing with scenarios

Build an Element, run actions and check every value and write.

What a scenario holds

A scenario is a JSON file in scenarios/. It builds an Element from inputs, runs actions in order and checks the results. The manifest lists every scenario file, and each file holds one scenario.

scenarios/heal.json
{
  "id": "heal-after-damage",
  "framework": "keeper",
  "parameters": { "name": "Mira", "max_hp": 10 },
  "expectedValues": { "name": "Mira", "max_hp": 10, "hp": 10 },
  "actions": [
    {
      "action": "hurt",
      "parameters": { "amount": 6 },
      "expectedWrites": [{ "op": "ledger.add", "key": "hp", "value": -6 }]
    },
    {
      "action": "heal",
      "parameters": { "amount": 9 },
      "expectedWrites": [{ "op": "ledger.set", "key": "hp", "value": 10 }]
    },
    { "action": "heal", "parameters": { "amount": 3 }, "expectedWrites": [] }
  ]
}
  1. Lines 4-5 Build the Element from these inputs and check its starting values.
  2. Line 6 actions lists the steps to run, in order.
  3. Line 10 The exact writes the step must make.
  4. Line 17 No writes: nothing changed.
FieldMeaning
idThe scenario's name.
frameworkThe id of the Framework to build.
parametersThe inputs to build it with.
expectedValuesEvery stored value right after construction.
expectedChildrenThe values of each child slot.
actionsSteps to run, at most 32 per scenario. Each has an action, its parameters and expectedWrites.
ledgerChecks of derived values.
listsSeeds a list with entries, by preset id.
expectedListsThe entries each list holds at the end, by preset id.
poolRollsRecorded rolls for a pool that uses the roll method, by pool id. See Rolled scores.
contentContent entries pinned to the built Element (kind, entryId, name and parameters), merged into the release as a character's pins are.

A step can use more fields:

Step fieldMeaning
chooseFor a list action: the entry the player picks for each ContentRef input, by preset id.
expectedRefusalThe refusal the step must end with. The step then expects no writes.
expectedValuesAfterStored and derived values after the step. Derived values are recalculated first.

Writes

expectedWrites lists every write the step makes, in order. Use ledger.set for a value that was set and ledger.add for an amount that was added. A step that changes nothing lists no writes, so the guard that skips a write is tested too.

If the real writes differ, the check shows both lists.

"expectedWrites": [{ "op": "ledger.set", "key": "hp", "value": 9 }]
Expected ordered writes [{"op":"ledger.set","key":"hp","value":9}]; observed [{"op":"ledger.set","key":"hp","value":10}].

Derived values and children

The ledger list checks a ledger function. It gives the function id, the value it starts from and the value it must return. expectedChildren checks the child Elements.

scenarios/new-keeper.json
  "expectedChildren": { "lamp": { "name": "Lantern", "oil": 5 } },
  1. Line 17 Checks the Elements in the child slots.
scenarios/new-keeper.json
  "ledger": [{ "function": "guard", "currentValue": 0, "expected": 3 }]
  1. Line 18 Calls the guard function with the current value 0 and expects 3.

What must be covered

  • Every constructor, action and ledger function needs a scenario.
  • Every action needs at least one step. Add a step for each branch you want proven, such as one that writes and one that writes nothing.
  • An action that adds to a list needs one accepted step and one step with expectedRefusal, whatever its offer says. The refused step proves that the offer can say no. An action that only removes from a list needs one accepted step.
  • The expected values must match what the constructor really returns, with the ledger functions already applied.

Steps are named after their scenario. A message such as heal-after-damage-2 means the third step of heal-after-damage, because the number counts from 0. Other messages count steps from 1.

A step that says it expects a refusal from an action that was not refused fails, and so does the opposite:

the step expects it to be refused with NOT_OFFERED
Add expectedRefusal

An action with no step fails with:

SELF_SCALAR_ACTION_SCENARIO_COVERAGE_MISSING: action keeper.hurt (Take damage) has no executable scenario steps.

A list action that lacks an accepted or a refused step fails with:

add a step with expectedRefusal, so its refused branch is proven.
add a step whose list.add is accepted, so its accepted branch is proven.

Expected values that differ from the constructor fail with:

Constructor output does not match its scenario.

Refusals

A list action can be refused. A step that expects it names the code in expectedRefusal, and lists no writes.

CodeWhen
NOT_OFFEREDThe picked entry is not among the entries the offer allows.
ALREADY_IN_LISTThe list already holds the entry.
LIST_FULLThe list is at its maximum size.
NOT_IN_LISTlist.remove names an entry the list does not hold.
LIST_CAPA limit that the list declares would be passed.
SAME_NAMEThe list already holds an entry with that name.

This project proves several of them. It also has a step where a guard stops the action, so the step expects no writes and no refusal.

scenarios/pack.json
{
  "id": "pack-trinkets",
  "framework": "keeper",
  "parameters": { "name": "Mira", "max_hp": 10 },
  "expectedValues": { "name": "Mira", "max_hp": 10, "hp": 10 },
  "actions": [
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "rope" },
      "expectedWrites": [{ "op": "list.add", "key": "pack", "value": "rope" }]
    },
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "rope" },
      "expectedRefusal": "ALREADY_IN_LIST",
      "expectedWrites": []
    },
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "anvil" },
      "expectedRefusal": "NOT_OFFERED",
      "expectedWrites": []
    },
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "candle" },
      "expectedWrites": [
        { "op": "list.add", "key": "pack", "value": "candle" }
      ]
    },
    {
      "action": "pack_trinket",
      "parameters": {},
      "choose": { "trinket": "bell" },
      "expectedWrites": []
    },
    {
      "action": "unpack_trinket",
      "parameters": {},
      "choose": { "trinket": "rope" },
      "expectedWrites": [
        { "op": "list.remove", "key": "pack", "value": "rope" }
      ]
    },
    {
      "action": "unpack_trinket",
      "parameters": {},
      "choose": { "trinket": "rope" },
      "expectedRefusal": "NOT_IN_LIST",
      "expectedWrites": []
    }
  ],
  "expectedLists": { "pack": ["candle"] }
}
  1. Lines 7-12 Accepted: list.add writes the rope.
  2. Lines 13-19 Refused: the list already holds the rope.
  3. Lines 20-26 Refused: the offer does not allow the anvil.
  4. Lines 35-40 The pack already holds two entries, so the guard writes nothing and no refusal is expected.
  5. Lines 41-48 Removed: the rope leaves the list.
  6. Lines 49-55 Refused: the rope is not in the list any more.
  7. Line 57 The pack ends with the candle only.

Running them

Check and preview in The Forge compiles the project and runs every scenario. Nothing is published, and no live character changes.

Next