fix(modules): bump the CLIENT half of MODULE_API_VERSION to 1.9.0
All checks were successful
PR Checks / client-build (pull_request) Successful in 26s
PR Checks / bot-tests (pull_request) Successful in 28s
PR Checks / server-tests (pull_request) Successful in 13m0s

The two halves version ONE contract and a test asserts they agree
(client/test/moduleRegistry.test.js). I bumped server/src/modules/version.js and
not client/src/modules/version.js, so client-build went red — the job runs the
client suite before it builds.

Nothing on the client half changed: a seed is server-side data and core's seeders
write it on the boot path. It bumps for the reason its own header gives — a
module declares one `coreApi` range against both halves, and a client claiming
1.8.0 while the server answers 1.9.0 is two answers to one question.

327 client tests green; client build clean.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-01 01:22:00 -05:00
parent 0a9149a04f
commit 40ab1ce8d2
2 changed files with 33 additions and 1 deletions

View File

@@ -11,6 +11,13 @@
// that the two files can drift, so a test asserts they agree
// (client/test/moduleRegistry.test.js) rather than trusting a bump to remember
// both.
// 1.9.0 - a module may ship its own message bodies and rules:
// `api.registerEngagementSeeds({ templates, ruleGroups })` (ENGAGEMENT.md Phase
// 11b, decision 7). Nothing on this half changed - a seed is server-side data
// and core's seeders write it on the boot path - but the bodies it ships are
// edited through the template editor this half already renders, and an operator
// meets them there. This file bumps for the reason at the top: the two halves
// state ONE version, and a module declares one `coreApi` range against both.
// 1.8.0 - the ceiling lattice gains `admin` (ENGAGEMENT.md Phase 11). Nothing on
// this half changed: a ceiling is declared on the server's `api` and enforced
// there, and the admin screens that render one read the vocabulary from
@@ -58,4 +65,4 @@
// but the two halves state ONE version: a module declares a single coreApi range
// and is served one chunk, so a client that claimed 1.0.0 while the server
// answered 1.1.0 would be two answers to one question.
export const MODULE_API_VERSION = '1.8.0'
export const MODULE_API_VERSION = '1.9.0'

View File

@@ -19,6 +19,7 @@ const { SEEDS, AMBIENT_VARIABLES, seedByKey } = require('../src/engagement/templ
const templatesDb = require('../src/model/engagement/engagementTemplates.db')
const settings = require('../src/model/settings/settings.model')
const templates = require('../src/engagement/templates')
const registries = require('../src/modules/registries')
const SITE = 'Runic Gateway'
const BASE = 'https://shard.example.com'
@@ -400,6 +401,30 @@ test('variablesFor answers from the seed for a template with no trigger, plus th
assert.deepEqual(templates.variablesFor({}).map((v) => v.name), ['siteName', 'siteUrl', 'logoUrl', 'year'])
})
test('a TRIGGER-bound template also gets the per-delivery variables', () => {
// Phase 11b. `emailChannel.deliver` computes an unsubscribe token per recipient
// and merges it last, so `{{unsubscribeUrl}}` has always RENDERED — but a
// trigger-bound template takes its variable list from the trigger, and a
// trigger has no business declaring a fact about how the mail was sent. Without
// this, a body carrying an unsubscribe line rendered correctly and then the
// save-time undeclared-variable check refused the first operator who edited it.
//
// Nothing had taken this path before: core's `notify.event` declares the
// variable in its own seed and is bound to no trigger.
registries.registerCore()
const names = templates.variablesFor({ trigger_id: 'news.post' }).map((v) => v.name)
assert.ok(names.includes('unsubscribeUrl'), 'a trigger-bound body may reference it')
assert.ok(names.includes('title'), 'and still gets the trigger\'s own')
assert.ok(names.includes('siteName'), 'and the ambient set')
// A SEEDLESS, triggerless template gets neither — there is no delivery to
// describe, and an unsubscribe link on a password reset is meaningless.
assert.equal(
templates.variablesFor({}).map((v) => v.name).includes('unsubscribeUrl'),
false,
)
})
// ── The seeder and the render entrypoint ────────────────────────────────────
test('the shipped default is used when the row is missing, and when it is unusable', async () => {