@ours.network/notifications 0.1.1
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/LICENSE +98 -0
- package/README.md +231 -0
- package/dist/cli.js +446 -0
- package/dist/config.js +104 -0
- package/dist/daemon.js +353 -0
- package/dist/http.js +31 -0
- package/dist/log-store.js +48 -0
- package/dist/presence.js +157 -0
- package/dist/provision.js +29 -0
- package/dist/push.js +6 -0
- package/dist/web-push-sender.js +41 -0
- package/mufl_code/20E39399689579D5AFA5132C59DEDDD328C3BCA1A74D546F2193B36585DDD283.muflo +0 -0
- package/mufl_code/config.mufl +21 -0
- package/mufl_code/core/.github/FUNDING.yml +3 -0
- package/mufl_code/core/.github/workflows/docs.yml +39 -0
- package/mufl_code/core/.github/workflows/tests.yml +66 -0
- package/mufl_code/core/COMMERCIAL-LICENCE.md +25 -0
- package/mufl_code/core/COMPATIBILITY.md +221 -0
- package/mufl_code/core/CONTRIBUTING.md +37 -0
- package/mufl_code/core/LICENSE +98 -0
- package/mufl_code/core/NOTIFICATIONS_CONTRACT.md +290 -0
- package/mufl_code/core/README.md +91 -0
- package/mufl_code/core/SECURITY.md +40 -0
- package/mufl_code/core/TRADEMARKS.md +24 -0
- package/mufl_code/core/a2a_capabilities.mm +449 -0
- package/mufl_code/core/a2a_cluster.mm +601 -0
- package/mufl_code/core/a2a_control.mm +90 -0
- package/mufl_code/core/a2a_messaging.mm +4817 -0
- package/mufl_code/core/a2a_monitoring.mm +94 -0
- package/mufl_code/core/a2a_notification_integration.mm +144 -0
- package/mufl_code/core/a2a_notifications.mm +1127 -0
- package/mufl_code/core/a2a_protocol.mm +246 -0
- package/mufl_code/core/a2a_versions.mm +801 -0
- package/mufl_code/core/config.mufl +29 -0
- package/mufl_code/core/docs/.vitepress/config.mts +58 -0
- package/mufl_code/core/docs/citations.json +688 -0
- package/mufl_code/core/docs/guide/01-vendor-the-core.md +50 -0
- package/mufl_code/core/docs/guide/02-configure-and-compile.md +100 -0
- package/mufl_code/core/docs/guide/03-wire-the-host.md +154 -0
- package/mufl_code/core/docs/guide/04-connect-and-message.md +156 -0
- package/mufl_code/core/docs/guide/05-test-your-app.md +75 -0
- package/mufl_code/core/docs/guide/index.md +63 -0
- package/mufl_code/core/docs/how-it-works/capabilities-and-control.md +77 -0
- package/mufl_code/core/docs/how-it-works/cluster.md +65 -0
- package/mufl_code/core/docs/how-it-works/identity.md +56 -0
- package/mufl_code/core/docs/how-it-works/invites-and-contacts.md +76 -0
- package/mufl_code/core/docs/how-it-works/messaging.md +55 -0
- package/mufl_code/core/docs/how-it-works/monitoring-and-config.md +72 -0
- package/mufl_code/core/docs/how-it-works/overview.md +45 -0
- package/mufl_code/core/docs/how-it-works/versioning.md +116 -0
- package/mufl_code/core/docs/index.md +27 -0
- package/mufl_code/core/docs/package-lock.json +3467 -0
- package/mufl_code/core/docs/package.json +15 -0
- package/mufl_code/core/docs/reference/contributing.md +26 -0
- package/mufl_code/core/docs/reference/glossary.md +108 -0
- package/mufl_code/core/docs/reference/implementations.md +23 -0
- package/mufl_code/core/docs/reference/modules.md +19 -0
- package/mufl_code/core/docs/scripts/check-citations.mjs +36 -0
- package/mufl_code/core/docs/scripts/check-denylist.mjs +30 -0
- package/mufl_code/core/docs/scripts/gen-llms.mjs +32 -0
- package/mufl_code/core/docs/workflows/cluster.md +89 -0
- package/mufl_code/core/docs/workflows/contact-exchange.md +60 -0
- package/mufl_code/core/docs/workflows/contact-restore.md +60 -0
- package/mufl_code/core/docs/workflows/control-verbs.md +70 -0
- package/mufl_code/core/docs/workflows/index.md +57 -0
- package/mufl_code/core/docs/workflows/introductions.md +60 -0
- package/mufl_code/core/docs/workflows/messaging.md +75 -0
- package/mufl_code/core/docs/workflows/monitoring.md +84 -0
- package/mufl_code/core/docs/workflows/notifications.md +109 -0
- package/mufl_code/core/release-notes/0.1.md +60 -0
- package/mufl_code/core/release-notes/0.3.md +22 -0
- package/mufl_code/core/release-notes/0.4.md +97 -0
- package/mufl_code/core/tests/README.md +83 -0
- package/mufl_code/core/tests/compat/README.md +82 -0
- package/mufl_code/core/tests/compat/compat.mjs +245 -0
- package/mufl_code/core/tests/compat/compat_peer.mjs +146 -0
- package/mufl_code/core/tests/compat/run_compat.sh +96 -0
- package/mufl_code/core/tests/corpus.mjs +139 -0
- package/mufl_code/core/tests/corpus_actor.mu +242 -0
- package/mufl_code/core/tests/fatcompile_actor.mu +52 -0
- package/mufl_code/core/tests/mig.mjs +805 -0
- package/mufl_code/core/tests/mig_actor.mu +518 -0
- package/mufl_code/core/tests/migapp.mjs +431 -0
- package/mufl_code/core/tests/migapp_actor.mu +294 -0
- package/mufl_code/core/tests/mufl_semantics/run.sh +69 -0
- package/mufl_code/core/tests/mufl_semantics/t11_three_version.mufl +47 -0
- package/mufl_code/core/tests/mufl_semantics/t2_extra_fields.mufl +11 -0
- package/mufl_code/core/tests/mufl_semantics/t7_runtime.mufl +30 -0
- package/mufl_code/core/tests/mufl_semantics/t9_passthrough.mufl +19 -0
- package/mufl_code/core/tests/mufl_semantics/t_str_order.mufl +19 -0
- package/mufl_code/core/tests/notif.mjs +755 -0
- package/mufl_code/core/tests/notif_actor.mu +450 -0
- package/mufl_code/core/tests/protocol_container.mm +18 -0
- package/mufl_code/core/tests/run.sh +65 -0
- package/mufl_code/core/tests/run_corpus.sh +51 -0
- package/mufl_code/core/tests/run_fatcompile.sh +40 -0
- package/mufl_code/core/tests/run_mig.sh +51 -0
- package/mufl_code/core/tests/run_migapp.sh +51 -0
- package/mufl_code/core/tests/run_notif.sh +57 -0
- package/mufl_code/core/tests/test.mjs +750 -0
- package/mufl_code/core/tests/test_actor.mu +564 -0
- package/mufl_code/core/tests/test_common.mjs +93 -0
- package/mufl_code/core/version.mm +24 -0
- package/mufl_code/packet.mu +170 -0
- package/mufl_code/protocol_container.mm +18 -0
- package/package.json +43 -0
- package/scripts/gen-vapid.mjs +16 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# tests/compat — cross-version back-compat matrix (M-legs) + upgrade-UX legs (U-legs)
|
|
2
|
+
|
|
3
|
+
The DR-rollout ship gate (DR-ROLLOUT-PLAN.md §5/§5.1): prove that an OLD-core peer and a
|
|
4
|
+
NEW-core peer interoperate (DoD D1) and that upgrades migrate seamlessly (D2/D3) — with the
|
|
5
|
+
two peers running in **separate processes, each under its own SDK runtime and its own
|
|
6
|
+
compiled unit**. That two-runtime split is what the rest of `tests/` deliberately avoids
|
|
7
|
+
(single node_modules, both peers in-process) and what production actually is mid-rollout.
|
|
8
|
+
|
|
9
|
+
## Peers
|
|
10
|
+
|
|
11
|
+
- **OLD** — core `v0.2.0` (`faa2b52`, the OSP: the deployed ours-mcp/tg-connector repo pin;
|
|
12
|
+
no `$pv`, no DR, legacy `encrypted_channel`), compiled by and run under an 0.9.x-era
|
|
13
|
+
toolkit (`OLD_ADAPT_TOOLKIT` / `OLD_SDK_NM`).
|
|
14
|
+
- **NEW** — THIS working tree's core, compiled by and run under the current pinned toolkit
|
|
15
|
+
(`ADAPT_TOOLKIT` / `OURS_SDK_NODE_MODULES`, same knobs as `run.sh`).
|
|
16
|
+
|
|
17
|
+
Each peer's build dir gets its own `core/` sources, its own compiled `.muflo`, and its own
|
|
18
|
+
`node_modules` symlink — a packet is only ever loaded by the runtime whose compiler built
|
|
19
|
+
it (the eval_unit.h hash-check contract).
|
|
20
|
+
|
|
21
|
+
## Env (run_compat.sh)
|
|
22
|
+
|
|
23
|
+
| Var | Meaning | Default |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `OLD_CORE_REF` | git ref for the OLD peer's core sources | `v0.2.0` |
|
|
26
|
+
| `OLD_ADAPT_TOOLKIT` | toolkit checkout whose `mufl-compile` + stdlib build the OLD unit | required |
|
|
27
|
+
| `OLD_SDK_NM` | node_modules with the OLD-era `@adapt-toolkit` sdk | required |
|
|
28
|
+
| `ADAPT_TOOLKIT` / `OURS_SDK_NODE_MODULES` / `DEV_BROKER` / `PORT` | as in `run.sh` | required |
|
|
29
|
+
|
|
30
|
+
## Leg inventory (status is explicit — an unimplemented leg FAILS, never skips silently)
|
|
31
|
+
|
|
32
|
+
| Leg | Gate | DoD | Status |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| M1 invite gen→redeem, both directions | MP | D1 | implemented |
|
|
35
|
+
| M2 first-contact message after redeem | MP | D1 | implemented |
|
|
36
|
+
| M3 steady-state send/receive both ways | MP | D1 | implemented |
|
|
37
|
+
| M4 file transfer both ways | MP | D1 | implemented |
|
|
38
|
+
| M5 OLD restarts: v0-blob export→reimport, channel resumes | MP | D1,D2 | implemented |
|
|
39
|
+
| M6 NEW restarts: stamped blob + DR sessions restore | MP | D1,D2 | implemented |
|
|
40
|
+
| M7 NEW↔NEW DR handshake + ratchet + dual restart | MP | D1 | not-implemented (needs 2× NEW peers — trivial config once M1–M6 green) |
|
|
41
|
+
| M8 stale-snapshot restore → self-heal | NTH | D2 | not-implemented |
|
|
42
|
+
| M9 corrupt blob import reject-to-empty | MP(NEW) | D2 | not-implemented |
|
|
43
|
+
| M10 version_error_t as data | NTH | D1 | not-implemented (assertion pattern exists in tests/test.mjs V-series) |
|
|
44
|
+
| M11 pv re-learning on peer downgrade | NTH | D1 | not-implemented |
|
|
45
|
+
| M12 cross-app tg-connector↔mcp | MP | D1,D4 | lives in the consumer repos (drives their packets, not this actor) |
|
|
46
|
+
| U1/U2/U3 npm-bump upgrade legs | MP | D2,D3 | live in the consumer repos' CI (install old → state → bump → verify) |
|
|
47
|
+
|
|
48
|
+
Related prior art this seeds from: control-plane `tests/blob-import.mjs` (old-unit→new-unit
|
|
49
|
+
same-seed gate) and `tests/mixed-unit.mjs` (4-cell pairing matrix) — single-runtime versions
|
|
50
|
+
of M5/M1; this harness generalizes them across runtimes.
|
|
51
|
+
|
|
52
|
+
## Run
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
OLD_ADAPT_TOOLKIT=... OLD_SDK_NM=... ADAPT_TOOLKIT=... OURS_SDK_NODE_MODULES=... \
|
|
56
|
+
DEV_BROKER=... bash tests/compat/run_compat.sh
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## M12 — control-plane NEW ↔ mcp OLD (owner's named leg) — GREEN 2026-07-21
|
|
60
|
+
|
|
61
|
+
Reuses this orchestrator with per-side flavors (`NEW_PEER_FLAVOR=messenger` switches the
|
|
62
|
+
READ verbs to `::actor::get_conversation`; the whole `::a2a_messaging::` write surface is
|
|
63
|
+
shared). Peers: NEW = the control-plane DR unit (dev5/dr-e2e-integration, sdk 0.10.10,
|
|
64
|
+
core v0.8.0) · OLD = the REAL mcp actor (`packages/core/mufl_code/actor.mu`) at its
|
|
65
|
+
deployed pins (core `faa2b52` = v0.2.0/OSP) compiled+run under published sdk/mufl 0.9.1.
|
|
66
|
+
Result: M1–M6 all green (invites both ways, first-contact, steady both ways, files both
|
|
67
|
+
ways, both restart-restore legs).
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
# build dirs: old = mcp actor + 0.9.1 stage; new = cp unit + its node_modules
|
|
71
|
+
OLD_BUILD_DIR=<mcp-old> NEW_BUILD_DIR=<cp-new> NEW_PEER_FLAVOR=messenger \
|
|
72
|
+
LEG_FILTER=M1,M2,M3,M4,M5,M6 node tests/compat/compat.mjs
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## U3 — control-plane upgrade path — GREEN 2026-07-21
|
|
76
|
+
|
|
77
|
+
Under the fresh-address ruling (unpublished app, zero clients) the npm-bump leg's
|
|
78
|
+
meaningful nucleus is the control-plane's own blob-import gate: main-era unit sources
|
|
79
|
+
(messenger + core 0.7.1) compiled with the CURRENT toolchain → real state → import under
|
|
80
|
+
the DR unit → same seed ⇒ same cid, history/contacts preserved, zero rejects, re-export
|
|
81
|
+
round-trips. Run via the cp repo's tests/run-blob-import.sh with OLD_UNIT pointed at the
|
|
82
|
+
main-era build.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Cross-version matrix orchestrator: spawns OLD-runtime and NEW-runtime peers
|
|
3
|
+
// (compat_peer.mjs each, separate processes, own node_modules + own unit — see README),
|
|
4
|
+
// then drives the M-legs against the pair. Scorecard semantics: an MP leg failing (or not
|
|
5
|
+
// implemented) exits non-zero; NTH legs report but never gate. No leg ever silently skips.
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { openSync as fsOpenSync, appendFileSync as fsAppendFileSync } from 'node:fs';
|
|
8
|
+
import * as readline from 'node:readline';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
const OLD_DIR = process.env.OLD_BUILD_DIR;
|
|
12
|
+
const NEW_DIR = process.env.NEW_BUILD_DIR;
|
|
13
|
+
const BROKER_URL = process.env.BROKER_URL || 'ws://127.0.0.1:9797';
|
|
14
|
+
if (!OLD_DIR || !NEW_DIR) { console.error('OLD_BUILD_DIR / NEW_BUILD_DIR required'); process.exit(2); }
|
|
15
|
+
|
|
16
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
17
|
+
let nextId = 1;
|
|
18
|
+
|
|
19
|
+
function spawnPeer(dir, name, extraEnv = {}) {
|
|
20
|
+
// stderr goes to a per-peer log — handshake rejects surface there, and
|
|
21
|
+
// discarding it turned real compat failures into blind timeouts.
|
|
22
|
+
const errLog = fsOpenSync(resolve(dir, `${name}.stderr.log`), 'a');
|
|
23
|
+
const child = spawn(process.execPath, [resolve(dir, 'compat_peer.mjs')], {
|
|
24
|
+
cwd: dir, stdio: ['pipe', 'pipe', errLog],
|
|
25
|
+
env: { ...process.env, BROKER_URL, PEER_NAME: name, PEER_SEED: `compat ${name} seed`,
|
|
26
|
+
PEER_FLAVOR: dir === NEW_DIR ? (process.env.NEW_PEER_FLAVOR || 'actor') : (process.env.OLD_PEER_FLAVOR || 'actor'),
|
|
27
|
+
...extraEnv },
|
|
28
|
+
});
|
|
29
|
+
const peer = { name, dir, child, cid: '', events: [], waiters: new Map(), readyP: null, exited: null };
|
|
30
|
+
const rl = readline.createInterface({ input: child.stdout });
|
|
31
|
+
let readyResolve; peer.readyP = new Promise((r) => { readyResolve = r; });
|
|
32
|
+
child.on('exit', (code, signal) => {
|
|
33
|
+
peer.exited = { code, signal };
|
|
34
|
+
console.log(` [peer ${name}] exited code=${code} signal=${signal} (${peer.waiters.size} call(s) in flight)`);
|
|
35
|
+
for (const [, w] of peer.waiters) w.reject(new Error(`peer ${name} exited (code=${code} signal=${signal})`));
|
|
36
|
+
peer.waiters.clear();
|
|
37
|
+
readyResolve(); // a dead peer resolves readyP; callers check peer.cid
|
|
38
|
+
});
|
|
39
|
+
rl.on('line', (line) => {
|
|
40
|
+
let msg; try { msg = JSON.parse(line); } catch {
|
|
41
|
+
// Non-JSON stdout (e.g. the sdk leak-detector dump on exit) must not
|
|
42
|
+
// vanish — append it to the peer's log so leak totals are auditable.
|
|
43
|
+
try { fsAppendFileSync(resolve(dir, `${name}.stderr.log`), `[stdout] ${line}\n`); } catch {}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (msg.ready) { peer.cid = msg.cid; return readyResolve(); }
|
|
47
|
+
if (msg.event) return void peer.events.push(msg.event);
|
|
48
|
+
if (msg.fatal) { console.error(`[${name}] FATAL ${msg.fatal}`); return readyResolve(); }
|
|
49
|
+
const w = peer.waiters.get(msg.id);
|
|
50
|
+
if (w) { peer.waiters.delete(msg.id); msg.ok ? w.resolve(msg.value) : w.reject(new Error(msg.error)); }
|
|
51
|
+
});
|
|
52
|
+
return peer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function call(peer, op, fields = {}, timeoutMs = 90000) {
|
|
56
|
+
const id = nextId++;
|
|
57
|
+
return new Promise((resolveP, rejectP) => {
|
|
58
|
+
if (peer.exited && op !== 'exit') return rejectP(new Error(`peer ${peer.name} already exited`));
|
|
59
|
+
const timer = setTimeout(() => {
|
|
60
|
+
peer.waiters.delete(id);
|
|
61
|
+
rejectP(new Error(`call ${op} on ${peer.name} timed out after ${timeoutMs}ms`));
|
|
62
|
+
}, timeoutMs);
|
|
63
|
+
peer.waiters.set(id, {
|
|
64
|
+
resolve: (v) => { clearTimeout(timer); resolveP(v); },
|
|
65
|
+
reject: (e) => { clearTimeout(timer); rejectP(e); },
|
|
66
|
+
});
|
|
67
|
+
peer.child.stdin.write(JSON.stringify({ id, op, ...fields }) + '\n');
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---- shared drive helpers --------------------------------------------------
|
|
72
|
+
async function contactsInclude(peer, cid, timeoutMs = 30000) {
|
|
73
|
+
const deadline = Date.now() + timeoutMs;
|
|
74
|
+
for (;;) {
|
|
75
|
+
const contacts = String(await call(peer, 'contacts').catch(() => ''));
|
|
76
|
+
if (contacts.includes(cid)) return true;
|
|
77
|
+
if (Date.now() > deadline) return false;
|
|
78
|
+
await sleep(1500);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function pair(inviter, redeemer) {
|
|
82
|
+
const inviteB64 = await call(inviter, 'invite', { name: redeemer.name });
|
|
83
|
+
await call(redeemer, 'redeem', { invite_b64: inviteB64, name: inviter.name });
|
|
84
|
+
// Handshake is multi-leg and cross-runtime — poll until BOTH sides list the
|
|
85
|
+
// peer instead of guessing a sleep. A timeout here is a real M1 verdict.
|
|
86
|
+
const a = await contactsInclude(inviter, redeemer.cid);
|
|
87
|
+
const b = await contactsInclude(redeemer, inviter.cid);
|
|
88
|
+
if (!a || !b) throw new Error(`pairing incomplete after 30s (inviter-sees=${a} redeemer-sees=${b})`);
|
|
89
|
+
}
|
|
90
|
+
async function roundTrip(from, to, text, expectRoute) {
|
|
91
|
+
const sent = await call(from, 'send', { cid: to.cid, text });
|
|
92
|
+
if (expectRoute && sent?.route !== expectRoute) {
|
|
93
|
+
throw new Error(`route mismatch: expected ${expectRoute}, got ${sent?.route} for "${text}"`);
|
|
94
|
+
}
|
|
95
|
+
await sleep(2500);
|
|
96
|
+
const inbox = await call(to, 'inbox', { cid: from.cid });
|
|
97
|
+
if (!String(inbox).includes(text)) throw new Error(`message not delivered: "${text}"`);
|
|
98
|
+
return sent;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- legs ------------------------------------------------------------------
|
|
102
|
+
const LEGS = [
|
|
103
|
+
{ id: 'M1', gate: 'MP', dod: 'D1', name: 'invite gen→redeem, both directions',
|
|
104
|
+
run: async (ctx) => {
|
|
105
|
+
await pair(ctx.oldPeer, ctx.newPeer); // OLD invites, NEW redeems (primary pair, reused by later legs)
|
|
106
|
+
// Reverse direction on a fresh throwaway pair: NEW invites, OLD redeems.
|
|
107
|
+
const old2 = spawnPeer(OLD_DIR, 'old2', { PEER_SEED: 'compat old2 seed' });
|
|
108
|
+
const new2 = spawnPeer(NEW_DIR, 'new2', { PEER_SEED: 'compat new2 seed' });
|
|
109
|
+
try {
|
|
110
|
+
await old2.readyP; await new2.readyP;
|
|
111
|
+
if (!old2.cid || !new2.cid) throw new Error('reverse-pair peers failed to boot');
|
|
112
|
+
await pair(new2, old2);
|
|
113
|
+
await roundTrip(new2, old2, 'reverse-pair first-contact new2→old2');
|
|
114
|
+
} finally {
|
|
115
|
+
await call(old2, 'exit').catch(() => {}); await call(new2, 'exit').catch(() => {});
|
|
116
|
+
} } },
|
|
117
|
+
{ id: 'M2', gate: 'MP', dod: 'D1', name: 'first-contact message after redeem (new→old)',
|
|
118
|
+
run: async (ctx) => { await roundTrip(ctx.newPeer, ctx.oldPeer, 'first-contact new->old'); } },
|
|
119
|
+
{ id: 'M3', gate: 'MP', dod: 'D1', name: 'steady-state send/receive both ways',
|
|
120
|
+
run: async (ctx) => {
|
|
121
|
+
await roundTrip(ctx.oldPeer, ctx.newPeer, 'steady old->new');
|
|
122
|
+
await roundTrip(ctx.newPeer, ctx.oldPeer, 'steady new->old'); } },
|
|
123
|
+
{ id: 'M4', gate: 'MP', dod: 'D1', name: 'file transfer both ways',
|
|
124
|
+
run: async (ctx) => {
|
|
125
|
+
const data_b64 = Buffer.from('compat file payload').toString('base64');
|
|
126
|
+
await call(ctx.oldPeer, 'send_file', { cid: ctx.newPeer.cid, filename: 'from-old.txt', data_b64 });
|
|
127
|
+
await call(ctx.newPeer, 'send_file', { cid: ctx.oldPeer.cid, filename: 'from-new.txt', data_b64 });
|
|
128
|
+
await sleep(3000);
|
|
129
|
+
const fNew = String(await call(ctx.newPeer, 'files', { cid: ctx.oldPeer.cid }));
|
|
130
|
+
const fOld = String(await call(ctx.oldPeer, 'files', { cid: ctx.newPeer.cid }));
|
|
131
|
+
if (!fNew.includes('from-old.txt')) throw new Error('old→new file not received');
|
|
132
|
+
if (!fOld.includes('from-new.txt')) throw new Error('new→old file not received'); } },
|
|
133
|
+
{ id: 'M5', gate: 'MP', dod: 'D1,D2', name: 'OLD restart: v0-blob export→reimport, channel resumes',
|
|
134
|
+
run: async (ctx) => {
|
|
135
|
+
await call(ctx.oldPeer, 'export_state', { file: resolve(OLD_DIR, 'state.bin') });
|
|
136
|
+
await ctx.respawn('oldPeer', true);
|
|
137
|
+
await roundTrip(ctx.newPeer, ctx.oldPeer, 'post-old-restart new->old'); } },
|
|
138
|
+
{ id: 'M6', gate: 'MP', dod: 'D1,D2', name: 'NEW restart: stamped blob + DR-session restore',
|
|
139
|
+
run: async (ctx) => {
|
|
140
|
+
await call(ctx.newPeer, 'export_state', { file: resolve(NEW_DIR, 'state.bin') });
|
|
141
|
+
await ctx.respawn('newPeer', true);
|
|
142
|
+
await roundTrip(ctx.oldPeer, ctx.newPeer, 'post-new-restart old->new'); } },
|
|
143
|
+
{ id: 'M7', gate: 'MP', dod: 'D1', name: 'NEW↔NEW DR handshake + ratchet + dual restart',
|
|
144
|
+
run: async () => {
|
|
145
|
+
const A = spawnPeer(NEW_DIR, 'newA', { PEER_SEED: 'compat newA seed' });
|
|
146
|
+
const B = spawnPeer(NEW_DIR, 'newB', { PEER_SEED: 'compat newB seed' });
|
|
147
|
+
try {
|
|
148
|
+
await A.readyP; await B.readyP;
|
|
149
|
+
if (!A.cid || !B.cid) throw new Error('M7 peers failed to boot');
|
|
150
|
+
const CAPS = { advertise: ['core.e2e', 'core.e2e.migrate'] };
|
|
151
|
+
await call(A, 'tx', { verb: '::actor::qa_init_caps', targ: CAPS });
|
|
152
|
+
await call(B, 'tx', { verb: '::actor::qa_init_caps', targ: CAPS });
|
|
153
|
+
await pair(A, B);
|
|
154
|
+
// Drive migration sweeps until the route flips to the double ratchet.
|
|
155
|
+
let route = '';
|
|
156
|
+
const deadline = Date.now() + 45000;
|
|
157
|
+
for (let i = 1; Date.now() < deadline && route !== 'e2e'; i++) {
|
|
158
|
+
await call(A, 'tx', { verb: '::a2a_messaging::sweep_e2e_migrations', targ: {} }).catch(() => {});
|
|
159
|
+
await call(B, 'tx', { verb: '::a2a_messaging::sweep_e2e_migrations', targ: {} }).catch(() => {});
|
|
160
|
+
await sleep(2000);
|
|
161
|
+
route = (await roundTrip(A, B, `dr-probe-${i}`))?.route ?? '';
|
|
162
|
+
}
|
|
163
|
+
if (route !== 'e2e') throw new Error(`route never reached e2e (last: ${route || 'legacy'})`);
|
|
164
|
+
// Ratchet advance both directions on the live session.
|
|
165
|
+
await roundTrip(A, B, 'dr-ratchet-a1', 'e2e');
|
|
166
|
+
await roundTrip(B, A, 'dr-ratchet-b1', 'e2e');
|
|
167
|
+
await roundTrip(A, B, 'dr-ratchet-a2', 'e2e');
|
|
168
|
+
// Dual restart with state — DR sessions must survive on BOTH ends.
|
|
169
|
+
await call(A, 'export_state', { file: resolve(NEW_DIR, 'stateA.bin') });
|
|
170
|
+
await call(B, 'export_state', { file: resolve(NEW_DIR, 'stateB.bin') });
|
|
171
|
+
await call(A, 'exit').catch(() => {}); await call(B, 'exit').catch(() => {});
|
|
172
|
+
await sleep(1000);
|
|
173
|
+
const A2 = spawnPeer(NEW_DIR, 'newA', { PEER_SEED: 'compat newA seed', PEER_STATE_FILE: resolve(NEW_DIR, 'stateA.bin') });
|
|
174
|
+
const B2 = spawnPeer(NEW_DIR, 'newB', { PEER_SEED: 'compat newB seed', PEER_STATE_FILE: resolve(NEW_DIR, 'stateB.bin') });
|
|
175
|
+
try {
|
|
176
|
+
await A2.readyP; await B2.readyP;
|
|
177
|
+
if (!A2.cid || !B2.cid) throw new Error('M7 respawn failed');
|
|
178
|
+
await sleep(2000);
|
|
179
|
+
await roundTrip(A2, B2, 'dr-post-restart-a');
|
|
180
|
+
await roundTrip(B2, A2, 'dr-post-restart-b');
|
|
181
|
+
} finally {
|
|
182
|
+
await call(A2, 'exit').catch(() => {}); await call(B2, 'exit').catch(() => {});
|
|
183
|
+
}
|
|
184
|
+
} finally {
|
|
185
|
+
await call(A, 'exit').catch(() => {}); await call(B, 'exit').catch(() => {});
|
|
186
|
+
} } },
|
|
187
|
+
{ id: 'M8', gate: 'NTH', dod: 'D2', name: 'stale-snapshot restore → self-heal',
|
|
188
|
+
run: async () => { throw new Error('NOT IMPLEMENTED'); } },
|
|
189
|
+
{ id: 'M9', gate: 'MP', dod: 'D2', name: 'corrupt blob import reject-to-functional (NEW)',
|
|
190
|
+
run: async (ctx) => {
|
|
191
|
+
await call(ctx.newPeer, 'export_state', { file: resolve(NEW_DIR, 'state.bin') });
|
|
192
|
+
const { readFileSync, writeFileSync } = await import('node:fs');
|
|
193
|
+
const good = readFileSync(resolve(NEW_DIR, 'state.bin'));
|
|
194
|
+
writeFileSync(resolve(NEW_DIR, 'state.bin'), good.subarray(0, Math.floor(good.length / 2)));
|
|
195
|
+
await ctx.respawn('newPeer', true);
|
|
196
|
+
const failed = ctx.newPeer.events.some((e) => String(e).startsWith('import_failed'));
|
|
197
|
+
if (!failed) throw new Error('corrupt blob did not surface import_failed');
|
|
198
|
+
// Reject-to-FUNCTIONAL: the fresh identity must still pair + message.
|
|
199
|
+
const inviteB64 = await call(ctx.oldPeer, 'invite', { name: 'renew' });
|
|
200
|
+
await call(ctx.newPeer, 'redeem', { invite_b64: inviteB64, name: 'oldpeer-again' });
|
|
201
|
+
const seen = await contactsInclude(ctx.newPeer, ctx.oldPeer.cid);
|
|
202
|
+
if (!seen) throw new Error('post-corruption re-pairing failed');
|
|
203
|
+
// Restore the good blob so later runs/legs are unaffected.
|
|
204
|
+
writeFileSync(resolve(NEW_DIR, 'state.bin'), good);
|
|
205
|
+
} },
|
|
206
|
+
{ id: 'M10', gate: 'NTH', dod: 'D1', name: 'version_error_t as data',
|
|
207
|
+
run: async () => { throw new Error('NOT IMPLEMENTED — assertion pattern in tests/test.mjs V-series'); } },
|
|
208
|
+
{ id: 'M11', gate: 'NTH', dod: 'D1', name: 'pv re-learning on peer downgrade',
|
|
209
|
+
run: async () => { throw new Error('NOT IMPLEMENTED'); } },
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
// ---- main ------------------------------------------------------------------
|
|
213
|
+
const ctx = {
|
|
214
|
+
oldPeer: spawnPeer(OLD_DIR, 'oldpeer'),
|
|
215
|
+
newPeer: spawnPeer(NEW_DIR, 'newpeer'),
|
|
216
|
+
async respawn(ref, withState) {
|
|
217
|
+
const prev = ctx[ref];
|
|
218
|
+
await call(prev, 'exit').catch(() => {});
|
|
219
|
+
await sleep(1000);
|
|
220
|
+
const dir = ref === 'oldPeer' ? OLD_DIR : NEW_DIR;
|
|
221
|
+
ctx[ref] = spawnPeer(dir, prev.name,
|
|
222
|
+
withState ? { PEER_STATE_FILE: resolve(dir, 'state.bin') } : {});
|
|
223
|
+
await ctx[ref].readyP;
|
|
224
|
+
if (!ctx[ref].cid) throw new Error(`${prev.name} failed to respawn`);
|
|
225
|
+
await sleep(2000);
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const results = [];
|
|
230
|
+
await ctx.oldPeer.readyP; await ctx.newPeer.readyP;
|
|
231
|
+
if (!ctx.oldPeer.cid || !ctx.newPeer.cid) { console.error('peer boot failed'); process.exit(1); }
|
|
232
|
+
console.log(`peers up: old=${ctx.oldPeer.cid.slice(0, 10)} new=${ctx.newPeer.cid.slice(0, 10)}`);
|
|
233
|
+
// LEG_FILTER=M7,M9 runs a subset (unlisted legs report as skipped-by-filter, never silently)
|
|
234
|
+
const filter = (process.env.LEG_FILTER || '').split(',').filter(Boolean);
|
|
235
|
+
for (const leg of LEGS) {
|
|
236
|
+
if (filter.length && !filter.includes(leg.id)) {
|
|
237
|
+
console.log(` - ${leg.id} skipped by LEG_FILTER (not a verdict)`); continue;
|
|
238
|
+
}
|
|
239
|
+
try { await leg.run(ctx); results.push({ leg, pass: true }); console.log(` ✓ ${leg.id} [${leg.gate}/${leg.dod}] ${leg.name}`); }
|
|
240
|
+
catch (err) { results.push({ leg, pass: false, err }); console.log(` ✗ ${leg.id} [${leg.gate}/${leg.dod}] ${leg.name}: ${err.message}`); }
|
|
241
|
+
}
|
|
242
|
+
await call(ctx.oldPeer, 'exit').catch(() => {}); await call(ctx.newPeer, 'exit').catch(() => {});
|
|
243
|
+
const mpFails = results.filter((r) => !r.pass && r.leg.gate === 'MP');
|
|
244
|
+
console.log(`\n${results.filter((r) => r.pass).length}/${LEGS.length} legs passed; MP failures: ${mpFails.length} (${mpFails.map((r) => r.leg.id).join(', ') || 'none'})`);
|
|
245
|
+
process.exit(mpFails.length ? 1 : 0);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Per-process peer driver for the compat matrix. One process = one runtime = one packet.
|
|
3
|
+
//
|
|
4
|
+
// Loaded from a build dir whose node_modules symlink points at THIS peer's sdk era and
|
|
5
|
+
// whose cwd holds THIS peer's compiled unit — the orchestrator (compat.mjs) never mixes
|
|
6
|
+
// runtimes in-process. Commands arrive as JSON lines on stdin; every command produces
|
|
7
|
+
// exactly one JSON line on stdout: {id, ok, value?, error?}. Async inbound traffic is
|
|
8
|
+
// surfaced via unsolicited {event} lines. Boot/mutate/ro shapes mirror tests/test_common.mjs
|
|
9
|
+
// (the era-stable driver conventions: add_client_message + on_return_data pending queue).
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as readline from 'node:readline';
|
|
13
|
+
import { adapt_wrapper } from '@adapt-toolkit/sdk/executables';
|
|
14
|
+
import { PacketWrapperConfigurator } from '@adapt-toolkit/sdk/wrappers';
|
|
15
|
+
import { object_to_adapt_value } from '@adapt-toolkit/sdk/wrapper';
|
|
16
|
+
|
|
17
|
+
const BROKER_URL = process.env.BROKER_URL || 'ws://127.0.0.1:9797';
|
|
18
|
+
const SEED = process.env.PEER_SEED || `compat seed ${process.env.PEER_NAME || 'peer'}`;
|
|
19
|
+
const STATE_FILE = process.env.PEER_STATE_FILE || ''; // set ⇒ import on boot if present
|
|
20
|
+
// PEER_FLAVOR=messenger switches the READ verbs to the control-plane messenger
|
|
21
|
+
// surface (history keyed per contact via ::actor::get_conversation). The whole
|
|
22
|
+
// ::a2a_messaging:: WRITE surface (invite/redeem/send/send_file) is shared.
|
|
23
|
+
const FLAVOR = process.env.PEER_FLAVOR || 'actor';
|
|
24
|
+
const UNIT_DIR = resolve('.');
|
|
25
|
+
const unitHash = fs.readdirSync(UNIT_DIR).find((f) => f.endsWith('.muflo')).slice(0, -'.muflo'.length);
|
|
26
|
+
const UNIT = new Uint8Array(fs.readFileSync(resolve(UNIT_DIR, `${unitHash}.muflo`)));
|
|
27
|
+
|
|
28
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
29
|
+
const out = (obj) => process.stdout.write(JSON.stringify(obj) + '\n');
|
|
30
|
+
const pending = [];
|
|
31
|
+
let pw = null;
|
|
32
|
+
|
|
33
|
+
function wire() {
|
|
34
|
+
pw.on_return_data = (d) => {
|
|
35
|
+
const kind = d.Reduce('kind').Visualize();
|
|
36
|
+
if (kind === 'notify_agent') {
|
|
37
|
+
try { out({ event: d.Reduce('payload').Reduce('event').Visualize() }); } catch { out({ event: 'unparsed' }); }
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (kind === 'save_state') return;
|
|
41
|
+
const w = pending.shift(); if (!w) return;
|
|
42
|
+
clearTimeout(w.timer); w.resolve(d.Reduce('payload'));
|
|
43
|
+
};
|
|
44
|
+
pw.on_transaction_failure = (msg) => {
|
|
45
|
+
const w = pending.shift();
|
|
46
|
+
if (w) { clearTimeout(w.timer); w.reject(new Error(msg)); }
|
|
47
|
+
else out({ event: `inbound-reject: ${String(msg).split('\n')[0]}` });
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mutate(name, targ) {
|
|
52
|
+
return new Promise((res, rej) => {
|
|
53
|
+
const timer = setTimeout(() => { rej(new Error(`timeout: ${name}`)); }, 20000);
|
|
54
|
+
pending.push({ resolve: res, reject: rej, timer });
|
|
55
|
+
pw.add_client_message(object_to_adapt_value({ name, targ }));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const ro = (name, targ) => pw.packet.ExecuteTransaction(object_to_adapt_value({ name, targ }));
|
|
59
|
+
const binv = (buf) => pw.packet.NewBinaryFromBuffer(Buffer.from(buf));
|
|
60
|
+
|
|
61
|
+
async function boot() {
|
|
62
|
+
const wrapper = await adapt_wrapper.start(['--broker_address', BROKER_URL, '--test_mode',
|
|
63
|
+
'--logger_config', '--level', 'WARNING', '--stdout', 'stderr', '--logger_config_end']);
|
|
64
|
+
wrapper.start();
|
|
65
|
+
await sleep(1500);
|
|
66
|
+
const cfg = new PacketWrapperConfigurator();
|
|
67
|
+
cfg.process_arguments(['--unit_hash', unitHash, '--seed_phrase', SEED, '--unit_dir_path', UNIT_DIR]);
|
|
68
|
+
await new Promise((res, rej) => {
|
|
69
|
+
const t = setTimeout(() => rej(new Error('create timeout')), 30000);
|
|
70
|
+
wrapper.packet_manager.create_packet(cfg, (p) => { clearTimeout(t); pw = p; wire(); res(); }, UNIT);
|
|
71
|
+
});
|
|
72
|
+
if (STATE_FILE && fs.existsSync(STATE_FILE)) {
|
|
73
|
+
// Import failure must not kill the peer — mirror the consumer posture
|
|
74
|
+
// (tg-connector: continue with the fresh identity, blob preserved): the
|
|
75
|
+
// M9 leg asserts exactly this reject-to-functional behavior.
|
|
76
|
+
try {
|
|
77
|
+
const bytes = fs.readFileSync(STATE_FILE);
|
|
78
|
+
await mutate('::actor::import_state', pw.packet.ParseValue(new Uint8Array(bytes)));
|
|
79
|
+
out({ event: 'state_imported' });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
out({ event: `import_failed: ${String(err).split('\n')[0]}` });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
out({ ready: true, cid: pw.packet.GetContainerID().Visualize() });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
88
|
+
rl.on('line', async (line) => {
|
|
89
|
+
let cmd;
|
|
90
|
+
try { cmd = JSON.parse(line); } catch { return out({ id: null, ok: false, error: 'bad json' }); }
|
|
91
|
+
const { id, op } = cmd;
|
|
92
|
+
try {
|
|
93
|
+
switch (op) {
|
|
94
|
+
case 'invite': {
|
|
95
|
+
const m = await mutate('::a2a_messaging::generate_invite', { name: cmd.name });
|
|
96
|
+
return out({ id, ok: true, value: Buffer.from(m.Reduce('invite').GetBinary()).toString('base64') });
|
|
97
|
+
}
|
|
98
|
+
case 'redeem': {
|
|
99
|
+
await mutate('::a2a_messaging::add_contact',
|
|
100
|
+
{ invite: binv(Buffer.from(cmd.invite_b64, 'base64')), name: cmd.name });
|
|
101
|
+
return out({ id, ok: true });
|
|
102
|
+
}
|
|
103
|
+
case 'send': {
|
|
104
|
+
const m = await mutate('::a2a_messaging::send_message', { contact: cmd.cid, text: cmd.text });
|
|
105
|
+
// route: 'e2e' (double ratchet) | 'migrating' | absent/NIL = legacy box
|
|
106
|
+
return out({ id, ok: true, value: { wire_id: m.Reduce('wire_id').Visualize(), route: m.Reduce('route').Visualize() } });
|
|
107
|
+
}
|
|
108
|
+
case 'tx': {
|
|
109
|
+
const m = await mutate(cmd.verb, cmd.targ);
|
|
110
|
+
return out({ id, ok: true, value: m ? m.Visualize() : null });
|
|
111
|
+
}
|
|
112
|
+
case 'send_file': {
|
|
113
|
+
const m = await mutate('::a2a_messaging::send_file', {
|
|
114
|
+
contact: cmd.cid, filename: cmd.filename, mime: cmd.mime || 'application/octet-stream',
|
|
115
|
+
data: binv(Buffer.from(cmd.data_b64, 'base64')),
|
|
116
|
+
});
|
|
117
|
+
return out({ id, ok: true, value: m.Reduce('wire_id').Visualize() });
|
|
118
|
+
}
|
|
119
|
+
case 'contacts': return out({ id, ok: true, value: ro('::a2a_messaging::list_contacts', undefined).Visualize() });
|
|
120
|
+
case 'inbox': {
|
|
121
|
+
if (FLAVOR === 'messenger') {
|
|
122
|
+
return out({ id, ok: true, value: ro('::actor::get_conversation', { contact: cmd.cid }).Visualize() });
|
|
123
|
+
}
|
|
124
|
+
return out({ id, ok: true, value: ro('::actor::list_incoming_messages', undefined).Visualize() });
|
|
125
|
+
}
|
|
126
|
+
case 'files': {
|
|
127
|
+
if (FLAVOR === 'messenger') {
|
|
128
|
+
// messenger file receipts land as history entries carrying $filename
|
|
129
|
+
return out({ id, ok: true, value: ro('::actor::get_conversation', { contact: cmd.cid }).Visualize() });
|
|
130
|
+
}
|
|
131
|
+
return out({ id, ok: true, value: ro('::actor::list_incoming_files', undefined).Visualize() });
|
|
132
|
+
}
|
|
133
|
+
case 'export_state': {
|
|
134
|
+
const exported = ro('::actor::export_state', undefined);
|
|
135
|
+
fs.writeFileSync(cmd.file, Buffer.from(exported.Serialize()));
|
|
136
|
+
return out({ id, ok: true });
|
|
137
|
+
}
|
|
138
|
+
case 'exit': out({ id, ok: true }); return process.exit(0);
|
|
139
|
+
default: return out({ id, ok: false, error: `unknown op ${op}` });
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return out({ id, ok: false, error: String(err) });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
boot().catch((e) => { out({ fatal: String(e) }); process.exit(1); });
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cross-version compat matrix: build TWO units — OLD core (git ref, OLD-era toolchain) and
|
|
3
|
+
# NEW core (this working tree, current toolchain) — each in its own build dir with its own
|
|
4
|
+
# node_modules symlink, boot ONE broker, run tests/compat/compat.mjs against the pair.
|
|
5
|
+
# See tests/compat/README.md for the env contract and leg inventory.
|
|
6
|
+
#
|
|
7
|
+
# Toolchain dirs auto-detect flavor (owner ruling: the OLD side should be the PUBLISHED
|
|
8
|
+
# @adapt-toolkit packages production actually pinned, not a from-scratch build):
|
|
9
|
+
# - npm @adapt-toolkit/mufl package root → prebuilds/linux-x64/mufl-compile
|
|
10
|
+
# - adapt toolkit checkout → build.<platform>.release/mufl-compile
|
|
11
|
+
set -u
|
|
12
|
+
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
13
|
+
CORE_DIR=$(cd "$HERE/../.." && pwd)
|
|
14
|
+
|
|
15
|
+
OLD_CORE_REF=${OLD_CORE_REF:-v0.2.0}
|
|
16
|
+
OLD_TOOLCHAIN=${OLD_TOOLCHAIN:?set OLD_TOOLCHAIN (npm @adapt-toolkit/mufl pkg root of the OLD era, e.g. <stage>/node_modules/@adapt-toolkit/mufl — or an old toolkit checkout)}
|
|
17
|
+
OLD_SDK_NM=${OLD_SDK_NM:?set OLD_SDK_NM (node_modules with the OLD-era @adapt-toolkit sdk, e.g. sdk@0.9.1)}
|
|
18
|
+
NEW_TOOLCHAIN=${NEW_TOOLCHAIN:-${ADAPT_TOOLKIT:-}}
|
|
19
|
+
[ -n "$NEW_TOOLCHAIN" ] || { echo "set NEW_TOOLCHAIN (current @adapt-toolkit/mufl pkg root or toolkit checkout)"; exit 2; }
|
|
20
|
+
SDK_NM=${OURS_SDK_NODE_MODULES:?set OURS_SDK_NODE_MODULES (current sdk node_modules)}
|
|
21
|
+
DEV_BROKER=${DEV_BROKER:?set DEV_BROKER (dev-broker.mjs launcher)}
|
|
22
|
+
PORT=${PORT:-9797}
|
|
23
|
+
|
|
24
|
+
# resolve_toolchain <dir> → sets RESOLVED_COMPILE / RESOLVED_ROOT
|
|
25
|
+
resolve_toolchain() {
|
|
26
|
+
local dir=$1 platform
|
|
27
|
+
platform="$(uname | tr '[:upper:]' '[:lower:]')"
|
|
28
|
+
for cand in "$dir/prebuilds/linux-x64/mufl-compile" \
|
|
29
|
+
"$dir/build/mufl-compile" \
|
|
30
|
+
"$dir/build.$platform.release/mufl-compile"; do
|
|
31
|
+
if [ -x "$cand" ]; then RESOLVED_COMPILE="$cand"; RESOLVED_ROOT="$dir"; return 0; fi
|
|
32
|
+
done
|
|
33
|
+
echo "MISSING: no mufl-compile under $dir"; exit 2
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for p in "$OLD_SDK_NM/@adapt-toolkit" "$SDK_NM/@adapt-toolkit" "$DEV_BROKER"; do
|
|
37
|
+
[ -e "$p" ] || { echo "MISSING: $p"; exit 2; }
|
|
38
|
+
done
|
|
39
|
+
|
|
40
|
+
WORK=$(mktemp -d); echo "WORK=$WORK (kept for post-mortem when KEEP_WORK=1)"; if [ "${KEEP_WORK:-0}" = "1" ]; then trap '[ -n "${BROKER_PID:-}" ] && kill "$BROKER_PID" 2>/dev/null' EXIT; else trap 'rm -rf "$WORK"; [ -n "${BROKER_PID:-}" ] && kill "$BROKER_PID" 2>/dev/null' EXIT; fi
|
|
41
|
+
|
|
42
|
+
# build_unit <outdir> <core_src_dir> <toolchain_dir> <sdk_nm>
|
|
43
|
+
# Uses the era's OWN tests/test_actor.mu + protocol_container.mm (the actor surface
|
|
44
|
+
# matches its core), same compile shape as tests/run.sh / consumer compile-mufl.sh.
|
|
45
|
+
build_unit() {
|
|
46
|
+
local out=$1 src=$2 tk=$3 nm=$4
|
|
47
|
+
resolve_toolchain "$tk"
|
|
48
|
+
mkdir -p "$out/core"
|
|
49
|
+
cp "$src"/*.mm "$out/core/"; cp "$src/config.mufl" "$out/core/" 2>/dev/null || true
|
|
50
|
+
cp "$src/tests/test_actor.mu" "$out/actor.mu"
|
|
51
|
+
# Cross-process peers must REGISTER with the broker (single-wrapper suites
|
|
52
|
+
# deliver locally and never need this): inject registration_proof, which the
|
|
53
|
+
# real consumer actors (messenger.mu, tg actor.mu) load explicitly and the
|
|
54
|
+
# core test actors deliberately omit. Without it, registration-message
|
|
55
|
+
# creation fails client-side and no traffic ever crosses the broker.
|
|
56
|
+
sed -i '0,/loads libraries/s//loads libraries\n registration_proof,/' "$out/actor.mu"
|
|
57
|
+
if [ -f "$src/tests/protocol_container.mm" ]; then cp "$src/tests/protocol_container.mm" "$out/"
|
|
58
|
+
else cp "$CORE_DIR/tests/protocol_container.mm" "$out/"; fi
|
|
59
|
+
ln -sfn "$nm" "$out/node_modules"
|
|
60
|
+
cp "$HERE/compat_peer.mjs" "$out/"
|
|
61
|
+
cat > "$out/config.mufl" <<'CFG'
|
|
62
|
+
config script
|
|
63
|
+
{
|
|
64
|
+
stdlib_config = (config_load #$MUFL_STDLIB_PATH).
|
|
65
|
+
core_config = (config_load #"core").
|
|
66
|
+
(
|
|
67
|
+
$imports ->
|
|
68
|
+
(
|
|
69
|
+
$libraries -> (stdlib_config $exports $libraries)'(core_config $exports $libraries)'($protocol_container -> #"protocol_container.mm"),
|
|
70
|
+
),
|
|
71
|
+
$exports -> ( $libraries -> (,), $applications -> (,) )
|
|
72
|
+
).
|
|
73
|
+
}
|
|
74
|
+
CFG
|
|
75
|
+
( cd "$out" && MUFL_STDLIB_PATH="$RESOLVED_ROOT/mufl_stdlib" "$RESOLVED_COMPILE" \
|
|
76
|
+
-mp "$RESOLVED_ROOT/meta" -mp "$RESOLVED_ROOT/transactions" -d-c actor.mu >/dev/null ) \
|
|
77
|
+
|| { echo "COMPILE FAILED in $out"; exit 3; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
echo "== building OLD unit ($OLD_CORE_REF, $(basename "$OLD_TOOLCHAIN")) =="
|
|
81
|
+
OLD_SRC="$WORK/old-src"; mkdir -p "$OLD_SRC"
|
|
82
|
+
git -C "$CORE_DIR" archive "$OLD_CORE_REF" | tar -x -C "$OLD_SRC"
|
|
83
|
+
build_unit "$WORK/old" "$OLD_SRC" "$OLD_TOOLCHAIN" "$OLD_SDK_NM"
|
|
84
|
+
|
|
85
|
+
echo "== building NEW unit (working tree, $(basename "$NEW_TOOLCHAIN")) =="
|
|
86
|
+
build_unit "$WORK/new" "$CORE_DIR" "$NEW_TOOLCHAIN" "$SDK_NM"
|
|
87
|
+
|
|
88
|
+
echo "== broker on :$PORT =="
|
|
89
|
+
# dev-broker contract: --host/--port REQUIRED, --test_mode skips attestation. Runs under
|
|
90
|
+
# the NEW sdk (cwd with the current node_modules) — the broker relays opaque frames, so
|
|
91
|
+
# one broker serves both eras.
|
|
92
|
+
( cd "$WORK/new" && node "$DEV_BROKER" --host 127.0.0.1 --port "$PORT" --test_mode ) & BROKER_PID=$!
|
|
93
|
+
sleep 2
|
|
94
|
+
|
|
95
|
+
BROKER_URL="ws://127.0.0.1:$PORT" OLD_BUILD_DIR="$WORK/old" NEW_BUILD_DIR="$WORK/new" \
|
|
96
|
+
node "$HERE/compat.mjs"
|