@bongos/core 1.19.640 → 1.19.641
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +24 -14
- package/docs/module-api-changelog.md +2 -0
- package/modules/discord/discord-bugs.js +5 -8
- package/modules/discord/discord-inbound.js +8 -15
- package/modules/discord/discord-reaction.js +6 -7
- package/modules/discord/lifecycle-port.js +69 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/discord_lifecycle_port.mjs +153 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.641",
|
|
6
|
+
"core_contract": "1.19.641",
|
|
7
|
+
"source_commit": "7af21a0086bc6cbc069a459b0a30b90f1646f82e",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-10T17:
|
|
9
|
+
"built_at": "2026-09-10T17:12:24.520Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 470,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2106,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2600,
|
|
20
|
+
"tree_sha256": "1fd37a4de38d4265a2f0956b03da63ce66a9e84de7ce8a24e1c66ae4465ea028",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/backlog-review/SKILL.md",
|
|
@@ -2772,7 +2772,7 @@
|
|
|
2772
2772
|
{
|
|
2773
2773
|
"path": "docs/module-api-changelog.md",
|
|
2774
2774
|
"mode": "0000644",
|
|
2775
|
-
"sha256": "
|
|
2775
|
+
"sha256": "967b335b7e06715fb8c07cea7af1141fa2dd3acb0da7873017048bb63aad15d0"
|
|
2776
2776
|
},
|
|
2777
2777
|
{
|
|
2778
2778
|
"path": "docs/modules-contract.md",
|
|
@@ -4297,7 +4297,7 @@
|
|
|
4297
4297
|
{
|
|
4298
4298
|
"path": "modules/discord/discord-bugs.js",
|
|
4299
4299
|
"mode": "0000644",
|
|
4300
|
-
"sha256": "
|
|
4300
|
+
"sha256": "000c766b483804e00ac523850e5cd8bc06a8f25aecf622a8629e1987014447b7"
|
|
4301
4301
|
},
|
|
4302
4302
|
{
|
|
4303
4303
|
"path": "modules/discord/discord-channels.js",
|
|
@@ -4307,7 +4307,7 @@
|
|
|
4307
4307
|
{
|
|
4308
4308
|
"path": "modules/discord/discord-inbound.js",
|
|
4309
4309
|
"mode": "0000644",
|
|
4310
|
-
"sha256": "
|
|
4310
|
+
"sha256": "4779118709c5e5365c1acc7b7348bf2d812726530876ac99d9f28777db0477b2"
|
|
4311
4311
|
},
|
|
4312
4312
|
{
|
|
4313
4313
|
"path": "modules/discord/discord-message-buffer.js",
|
|
@@ -4317,13 +4317,18 @@
|
|
|
4317
4317
|
{
|
|
4318
4318
|
"path": "modules/discord/discord-reaction.js",
|
|
4319
4319
|
"mode": "0000644",
|
|
4320
|
-
"sha256": "
|
|
4320
|
+
"sha256": "6d5cd39f63c1f8d6d84e713606844eead96fc2366e720523067f46d845c10c8d"
|
|
4321
4321
|
},
|
|
4322
4322
|
{
|
|
4323
4323
|
"path": "modules/discord/discord-role-sync.js",
|
|
4324
4324
|
"mode": "0000644",
|
|
4325
4325
|
"sha256": "852abc58bdec011b6d3bf222a35e198c6ea98a761d57a76be85477ef85ae54f5"
|
|
4326
4326
|
},
|
|
4327
|
+
{
|
|
4328
|
+
"path": "modules/discord/lifecycle-port.js",
|
|
4329
|
+
"mode": "0000644",
|
|
4330
|
+
"sha256": "f5c7bb703d5c230f334322128129dfc02c6cc03fa9d8d37e00b67493de80a25d"
|
|
4331
|
+
},
|
|
4327
4332
|
{
|
|
4328
4333
|
"path": "modules/discord/migrations/discord_001_board_messages.sql",
|
|
4329
4334
|
"mode": "0000644",
|
|
@@ -7717,12 +7722,12 @@
|
|
|
7717
7722
|
{
|
|
7718
7723
|
"path": "package-lock.json",
|
|
7719
7724
|
"mode": "0000644",
|
|
7720
|
-
"sha256": "
|
|
7725
|
+
"sha256": "9a655dfca70f3edf6083c6533004860a8a6da53455d732526dea6c0c37f3fcf1"
|
|
7721
7726
|
},
|
|
7722
7727
|
{
|
|
7723
7728
|
"path": "package.json",
|
|
7724
7729
|
"mode": "0000644",
|
|
7725
|
-
"sha256": "
|
|
7730
|
+
"sha256": "80d6ffe532ebbd4d2165171ec5e2887487599e42c53e5411bb4048074d2eb0fb"
|
|
7726
7731
|
},
|
|
7727
7732
|
{
|
|
7728
7733
|
"path": "public-docs/index.html",
|
|
@@ -9477,7 +9482,7 @@
|
|
|
9477
9482
|
{
|
|
9478
9483
|
"path": "src/module-api.js",
|
|
9479
9484
|
"mode": "0000644",
|
|
9480
|
-
"sha256": "
|
|
9485
|
+
"sha256": "cc274e241228f0ad4bb534674500d4daf228abcf6434241fed7152e49590f43f"
|
|
9481
9486
|
},
|
|
9482
9487
|
{
|
|
9483
9488
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10389,6 +10394,11 @@
|
|
|
10389
10394
|
"mode": "0000644",
|
|
10390
10395
|
"sha256": "c4e01ac01a3f01fc1a7aece34457a1660034164ab7cb65039f26d4a72d92a403"
|
|
10391
10396
|
},
|
|
10397
|
+
{
|
|
10398
|
+
"path": "tests/discord_lifecycle_port.mjs",
|
|
10399
|
+
"mode": "0000644",
|
|
10400
|
+
"sha256": "eaa289f4e1fa413b30f23cc23a0aea166bd1d9c18a1992d07a0694b28c9fa3c2"
|
|
10401
|
+
},
|
|
10392
10402
|
{
|
|
10393
10403
|
"path": "tests/discord_message_buffer.mjs",
|
|
10394
10404
|
"mode": "0000644",
|
|
@@ -1729,5 +1729,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1729
1729
|
landed since 1.19.638 with no explicit bump. run 34502609272. (task 1002620)
|
|
1730
1730
|
1.19.640 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1731
1731
|
landed since 1.19.639 with no explicit bump. run 34506041899. (task 1002620)
|
|
1732
|
+
1.19.641 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1733
|
+
landed since 1.19.640 with no explicit bump. run 34506718627. (task 1002620)
|
|
1732
1734
|
---------------------------------------------------------------------------
|
|
1733
1735
|
```
|
|
@@ -57,6 +57,7 @@ const { auditDiscordBot } = require('./discord-audit');
|
|
|
57
57
|
const auth = require('./auth');
|
|
58
58
|
const attachments = require('./bug-attachments');
|
|
59
59
|
const { combineParts, createMessageAggregator } = require('./discord-message-buffer');
|
|
60
|
+
const { lifecycleMethod } = require('./lifecycle-port');
|
|
60
61
|
const api = require('../../src/module-api');
|
|
61
62
|
const { LIMITS } = api;
|
|
62
63
|
const log = api.logger('discord');
|
|
@@ -134,14 +135,10 @@ const realDeps = {
|
|
|
134
135
|
// module rule, ADR 0083/0093). `ideas.capture` is the same door #ideas uses;
|
|
135
136
|
// `lifecycle` answers which goal, on which still-open version.
|
|
136
137
|
captureIdea: (...a) => api.resolve('ideas.capture')(...a),
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
suggestGoalForCapture:
|
|
141
|
-
const lifecycle = api.resolveOptional && api.resolveOptional('lifecycle');
|
|
142
|
-
if (!lifecycle || !lifecycle.suggestGoalForCapture) return null;
|
|
143
|
-
return lifecycle.suggestGoalForCapture(...a);
|
|
144
|
-
},
|
|
138
|
+
// Optional, not required: an instance running without the lifecycle module has
|
|
139
|
+
// no goals to suggest, and that is a reason to file the report WITHOUT a home
|
|
140
|
+
// — never a reason to lose it. Resolves null, which lands it in the inbox.
|
|
141
|
+
suggestGoalForCapture: lifecycleMethod('suggestGoalForCapture', () => null),
|
|
145
142
|
insertAuditLog: db.insertAuditLog,
|
|
146
143
|
persistImages: attachments.persistImages,
|
|
147
144
|
};
|
|
@@ -49,6 +49,7 @@ const { auditDiscordBot } = require('./discord-audit');
|
|
|
49
49
|
const auth = require('./auth');
|
|
50
50
|
const attachments = require('./bug-attachments');
|
|
51
51
|
const { createMessageAggregator } = require('./discord-message-buffer');
|
|
52
|
+
const { lifecycleMethod } = require('./lifecycle-port');
|
|
52
53
|
const { LIMITS } = api;
|
|
53
54
|
const log = api.logger('discord');
|
|
54
55
|
|
|
@@ -88,21 +89,13 @@ const realDeps = {
|
|
|
88
89
|
// Lazy: resolve the `ideas.capture` port at CALL time, so the ideas module's
|
|
89
90
|
// registration (at its route-factory mount) need not precede this module's load.
|
|
90
91
|
captureIdea: (...a) => api.resolve('ideas.capture')(...a),
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
suggestGoalForCapture:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
},
|
|
99
|
-
// Same resolveOptional posture — a vanilla instance without lifecycle has no
|
|
100
|
-
// versions to default to, which is a reason to skip the default, never the idea.
|
|
101
|
-
listVersions: async () => {
|
|
102
|
-
const lifecycle = api.resolveOptional && api.resolveOptional('lifecycle');
|
|
103
|
-
if (!lifecycle || !lifecycle.listVersions) return [];
|
|
104
|
-
return lifecycle.listVersions();
|
|
105
|
-
},
|
|
92
|
+
// Optional, not required: a vanilla instance without the lifecycle module has
|
|
93
|
+
// no goals to suggest, and that is a reason to skip the hint — never a reason
|
|
94
|
+
// to lose the idea (the discord-bugs.js precedent).
|
|
95
|
+
suggestGoalForCapture: lifecycleMethod('suggestGoalForCapture', () => null),
|
|
96
|
+
// Same posture — a vanilla instance without lifecycle has no versions to
|
|
97
|
+
// default to, which is a reason to skip the default, never the idea.
|
|
98
|
+
listVersions: lifecycleMethod('listVersions', () => []),
|
|
106
99
|
insertAuditLog: db.insertAuditLog,
|
|
107
100
|
persistImages: attachments.persistImages,
|
|
108
101
|
};
|
|
@@ -32,14 +32,13 @@ const { pool } = require('../../src/module-api');
|
|
|
32
32
|
const db = require('./db');
|
|
33
33
|
const { auditDiscordBot } = require('./discord-audit');
|
|
34
34
|
const auth = require('./auth');
|
|
35
|
-
const
|
|
36
|
-
// VOTABLE_TASK_STATUSES is the lifecycle module's votable-status set,
|
|
35
|
+
const { lifecycleMember } = require('./lifecycle-port');
|
|
36
|
+
// VOTABLE_TASK_STATUSES is the lifecycle module's votable-status set, read via
|
|
37
37
|
// the `lifecycle` kernel PORT (the doorway leak retired at BV1.R86) — a vote only
|
|
38
|
-
// lands on confirmed/shipped work.
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
const votableStatuses = () => api.resolveOptional('lifecycle')?.VOTABLE_TASK_STATUSES || new Set();
|
|
38
|
+
// lands on confirmed/shipped work. lifecycle-port.js keeps the resolve late and
|
|
39
|
+
// the absent-module fallback empty. A #ship-feed message is always about a
|
|
40
|
+
// shipped task, but we re-check rather than trust the channel.
|
|
41
|
+
const votableStatuses = () => lifecycleMember('VOTABLE_TASK_STATUSES', () => new Set());
|
|
43
42
|
const VOTING_RANKS = new Set(['metic', 'archon']);
|
|
44
43
|
|
|
45
44
|
// PURE — extract the GDS task id from a #ship-feed message. The producer
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// modules/discord/lifecycle-port.js — the one place this module reaches the
|
|
2
|
+
// OPTIONAL `lifecycle` kernel port.
|
|
3
|
+
//
|
|
4
|
+
// Why this file exists: four call sites across discord-bugs.js,
|
|
5
|
+
// discord-inbound.js and discord-reaction.js each hand-rolled the same
|
|
6
|
+
// resolveOptional('lifecycle') dance — resolve the port at call time, check the
|
|
7
|
+
// member is actually there, fall back to an empty value when it is not. The
|
|
8
|
+
// duplication was a grader nit on task 1003068 and the goal 1000071 R11 ship.
|
|
9
|
+
//
|
|
10
|
+
// THE POSTURE — resolveOptional, never resolve. `lifecycle` is an optional
|
|
11
|
+
// module: a vanilla Cloud Bongos instance runs without it, so it has no goals
|
|
12
|
+
// to suggest and no versions to default to. That is a reason to skip the hint,
|
|
13
|
+
// NEVER a reason to lose the idea or drop the bug report. Every accessor here
|
|
14
|
+
// degrades to an empty value, and nothing in this file throws.
|
|
15
|
+
//
|
|
16
|
+
// LATE BINDING IS LOAD-BEARING. The port is resolved inside each accessor, not
|
|
17
|
+
// once at require time, because this module's files are loaded before the
|
|
18
|
+
// lifecycle module registers its provider at boot. Hoisting the resolve to
|
|
19
|
+
// module scope would pin the fallback forever — which is why every accessor
|
|
20
|
+
// below is a function, and why none of them caches.
|
|
21
|
+
//
|
|
22
|
+
// A module may import only the published doorway (src/module-api.js), so this
|
|
23
|
+
// helper lives beside its callers rather than in core (ADR 0083/0093).
|
|
24
|
+
|
|
25
|
+
const api = require('../../src/module-api');
|
|
26
|
+
|
|
27
|
+
// The port itself, or null. The `api.resolveOptional &&` guard is kept because
|
|
28
|
+
// three of the four original copies carried it; discord-reaction.js was the odd
|
|
29
|
+
// one out, so routing every caller through here also settles that inconsistency
|
|
30
|
+
// in favour of the guard.
|
|
31
|
+
function lifecyclePort() {
|
|
32
|
+
return (api.resolveOptional && api.resolveOptional('lifecycle')) || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Read one member off the `lifecycle` port, or produce an empty stand-in.
|
|
36
|
+
//
|
|
37
|
+
// `makeFallback` is a FACTORY, not a value, and deliberately so: the fallbacks
|
|
38
|
+
// in play are mutable (`[]`, `new Set()`), and handing every caller one shared
|
|
39
|
+
// instance would turn a mutation by any one of them into a cross-call leak.
|
|
40
|
+
// Each call gets a fresh fallback, exactly as the hand-rolled copies did.
|
|
41
|
+
//
|
|
42
|
+
// Reading as `lifecycle[member]` also keeps the port's GETTERS working —
|
|
43
|
+
// VOTABLE_TASK_STATUSES is one (`get VOTABLE_TASK_STATUSES()`), so it must be
|
|
44
|
+
// read off the object rather than destructured ahead of time.
|
|
45
|
+
function lifecycleMember(member, makeFallback) {
|
|
46
|
+
const lifecycle = lifecyclePort();
|
|
47
|
+
const value = lifecycle ? lifecycle[member] : undefined;
|
|
48
|
+
return value === undefined || value === null ? makeFallback() : value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Wrap an optional lifecycle METHOD as an always-callable async function: it
|
|
52
|
+
// forwards to the port when the module is present, and resolves to a fresh
|
|
53
|
+
// fallback when either the module or that method is absent. The caller cannot
|
|
54
|
+
// tell the two absences apart, and does not need to.
|
|
55
|
+
//
|
|
56
|
+
// `fn.call(lifecycle, …)`, not `fn(…)`: the call sites this replaced invoked
|
|
57
|
+
// `lifecycle.method(...)`, so the port was the receiver. Every member the port
|
|
58
|
+
// exports today is a delegating arrow that ignores `this`, so nothing currently
|
|
59
|
+
// depends on it — which is exactly why dropping it would go unnoticed until a
|
|
60
|
+
// provider that DOES use `this` broke silently. Preserve it.
|
|
61
|
+
function lifecycleMethod(member, makeFallback) {
|
|
62
|
+
return async (...args) => {
|
|
63
|
+
const lifecycle = lifecyclePort();
|
|
64
|
+
const fn = lifecycle ? lifecycle[member] : undefined;
|
|
65
|
+
return typeof fn === 'function' ? fn.call(lifecycle, ...args) : makeFallback();
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { lifecycleMember, lifecycleMethod };
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.641",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.641",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.641",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.641'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// tests/discord_lifecycle_port.mjs — the optional `lifecycle` port accessor
|
|
2
|
+
// (task 1003630).
|
|
3
|
+
//
|
|
4
|
+
// The four hand-rolled resolveOptional('lifecycle') wrappers this helper
|
|
5
|
+
// replaced were never covered: every existing discord test injects `deps`, so
|
|
6
|
+
// the real `realDeps` wrappers — and specifically their absent-module branch —
|
|
7
|
+
// ran only in production. That branch is the whole point of the file (a vanilla
|
|
8
|
+
// Cloud Bongos instance has no lifecycle module), so it is what these pin:
|
|
9
|
+
//
|
|
10
|
+
// 1. present module → the port's own member/method is used
|
|
11
|
+
// 2. absent module → a fresh empty fallback, and NO throw
|
|
12
|
+
// 3. absent MEMBER on a present module → the same fallback (the caller cannot
|
|
13
|
+
// tell the two absences apart, and must not need to)
|
|
14
|
+
// 4. the fallback is a FACTORY — each call gets its own instance, so one
|
|
15
|
+
// caller mutating an empty [] or Set() cannot leak into the next
|
|
16
|
+
// 5. binding is LATE — a provider registered after require() is still seen
|
|
17
|
+
// 6. the RECEIVER survives — `lifecycle.method(...)` was the original call
|
|
18
|
+
// shape, so `this` inside a method must still be the port, and a getter
|
|
19
|
+
// member must still fire
|
|
20
|
+
//
|
|
21
|
+
// DB-free and bot-free: it drives the seam registry directly.
|
|
22
|
+
//
|
|
23
|
+
// WHY IT LIVES HERE and not in modules/discord/tests/: module-owned tests are
|
|
24
|
+
// discovered but gated by isModuleEnabled('discord'), and `discord` is OFF in
|
|
25
|
+
// the vanilla pack (config/modules.neutral.json) with no config/modules.json in
|
|
26
|
+
// the repo — so a module-owned copy is skipped in CI and would never actually
|
|
27
|
+
// run. The runner's MODULE_TESTS map is curated and does not list discord, so
|
|
28
|
+
// top-level tests/discord_*.mjs run unconditionally. This sits beside its three
|
|
29
|
+
// siblings (discord_reaction, discord_inbound, discord_bugs) for that reason.
|
|
30
|
+
|
|
31
|
+
import { strict as assert } from 'node:assert';
|
|
32
|
+
import { createRequire } from 'node:module';
|
|
33
|
+
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
const seams = require('../src/module-seams.js');
|
|
36
|
+
const { lifecycleMember, lifecycleMethod } = require('../modules/discord/lifecycle-port.js');
|
|
37
|
+
|
|
38
|
+
let passed = 0;
|
|
39
|
+
let failed = 0;
|
|
40
|
+
async function test(name, fn) {
|
|
41
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
42
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('\ndiscord: the optional lifecycle port degrades instead of throwing');
|
|
46
|
+
|
|
47
|
+
// ---- 1. the module is present ---------------------------------------------
|
|
48
|
+
|
|
49
|
+
await test('lifecycleMember reads the real member off a registered port', () => {
|
|
50
|
+
seams._reset();
|
|
51
|
+
const statuses = new Set(['confirmed', 'shipped']);
|
|
52
|
+
seams.registerProvider('lifecycle', { VOTABLE_TASK_STATUSES: statuses });
|
|
53
|
+
assert.equal(lifecycleMember('VOTABLE_TASK_STATUSES', () => new Set()), statuses);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await test('lifecycleMethod forwards args to the real method and returns its value', async () => {
|
|
57
|
+
seams._reset();
|
|
58
|
+
const seen = [];
|
|
59
|
+
seams.registerProvider('lifecycle', {
|
|
60
|
+
suggestGoalForCapture: async (draft, extra) => { seen.push([draft, extra]); return { goal_id: '1000091' }; },
|
|
61
|
+
});
|
|
62
|
+
const suggest = lifecycleMethod('suggestGoalForCapture', () => null);
|
|
63
|
+
assert.deepEqual(await suggest({ title: 'x' }, 7), { goal_id: '1000091' });
|
|
64
|
+
assert.deepEqual(seen, [[{ title: 'x' }, 7]], 'every argument is forwarded, in order');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ---- 2. the module is absent (the vanilla instance) ------------------------
|
|
68
|
+
|
|
69
|
+
await test('lifecycleMember falls back when NO provider is registered', () => {
|
|
70
|
+
seams._reset();
|
|
71
|
+
const out = lifecycleMember('VOTABLE_TASK_STATUSES', () => new Set());
|
|
72
|
+
assert.ok(out instanceof Set);
|
|
73
|
+
assert.equal(out.size, 0, 'an empty votable set, so no vote can land');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await test('lifecycleMethod resolves the fallback rather than throwing', async () => {
|
|
77
|
+
seams._reset();
|
|
78
|
+
assert.equal(await lifecycleMethod('suggestGoalForCapture', () => null)({ title: 'x' }), null);
|
|
79
|
+
assert.deepEqual(await lifecycleMethod('listVersions', () => [])(), []);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ---- 3. present module, absent member -------------------------------------
|
|
83
|
+
|
|
84
|
+
await test('a present port MISSING the member falls back the same way', async () => {
|
|
85
|
+
seams._reset();
|
|
86
|
+
seams.registerProvider('lifecycle', { somethingElse: () => 'nope' });
|
|
87
|
+
assert.deepEqual(lifecycleMember('VOTABLE_TASK_STATUSES', () => new Set()), new Set());
|
|
88
|
+
assert.equal(await lifecycleMethod('suggestGoalForCapture', () => null)({}), null);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await test('a member present but NOT a function still falls back', async () => {
|
|
92
|
+
seams._reset();
|
|
93
|
+
seams.registerProvider('lifecycle', { listVersions: 'not callable' });
|
|
94
|
+
assert.deepEqual(await lifecycleMethod('listVersions', () => [])(), []);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ---- 4. the fallback is a factory, not a shared instance -------------------
|
|
98
|
+
|
|
99
|
+
await test('each fallback is a FRESH instance — a mutation cannot leak forward', async () => {
|
|
100
|
+
seams._reset();
|
|
101
|
+
const first = lifecycleMember('VOTABLE_TASK_STATUSES', () => new Set());
|
|
102
|
+
first.add('poisoned');
|
|
103
|
+
const second = lifecycleMember('VOTABLE_TASK_STATUSES', () => new Set());
|
|
104
|
+
assert.equal(second.size, 0, 'the second caller must not see the first caller\'s write');
|
|
105
|
+
assert.notEqual(first, second);
|
|
106
|
+
|
|
107
|
+
const listVersions = lifecycleMethod('listVersions', () => []);
|
|
108
|
+
const a = await listVersions();
|
|
109
|
+
a.push('poisoned');
|
|
110
|
+
assert.deepEqual(await listVersions(), [], 'the same holds across method calls');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ---- 6. the receiver and getters survive the indirection -------------------
|
|
114
|
+
|
|
115
|
+
await test('a method still sees the PORT as `this` (not undefined)', async () => {
|
|
116
|
+
seams._reset();
|
|
117
|
+
const port = {
|
|
118
|
+
marker: 'the-port',
|
|
119
|
+
// A plain method, so `this` is meaningful — unlike the delegating arrows the
|
|
120
|
+
// real lifecycle module exports today.
|
|
121
|
+
async suggestGoalForCapture() { return this && this.marker; },
|
|
122
|
+
};
|
|
123
|
+
seams.registerProvider('lifecycle', port);
|
|
124
|
+
assert.equal(
|
|
125
|
+
await lifecycleMethod('suggestGoalForCapture', () => null)({}),
|
|
126
|
+
'the-port',
|
|
127
|
+
'fn.call(lifecycle, …) — dropping the receiver would return undefined here',
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await test('a GETTER member is read off the object, so it actually fires', () => {
|
|
132
|
+
seams._reset();
|
|
133
|
+
let reads = 0;
|
|
134
|
+
seams.registerProvider('lifecycle', {
|
|
135
|
+
get VOTABLE_TASK_STATUSES() { reads += 1; return new Set(['shipped']); },
|
|
136
|
+
});
|
|
137
|
+
assert.deepEqual(lifecycleMember('VOTABLE_TASK_STATUSES', () => new Set()), new Set(['shipped']));
|
|
138
|
+
assert.equal(reads, 1, 'the real port defines this member as a getter');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ---- 5. binding is late ----------------------------------------------------
|
|
142
|
+
|
|
143
|
+
await test('a provider registered AFTER require() is still picked up', async () => {
|
|
144
|
+
seams._reset();
|
|
145
|
+
const suggest = lifecycleMethod('suggestGoalForCapture', () => null);
|
|
146
|
+
assert.equal(await suggest({}), null, 'nothing registered yet — the fallback');
|
|
147
|
+
seams.registerProvider('lifecycle', { suggestGoalForCapture: async () => ({ goal_id: '42' }) });
|
|
148
|
+
assert.deepEqual(await suggest({}), { goal_id: '42' }, 'the late registration is seen without a re-require');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
seams._reset();
|
|
152
|
+
console.log(`\n ${passed} passed, ${failed} failed`);
|
|
153
|
+
if (failed > 0) process.exit(1);
|