@junghanacs/entwurf 0.12.0
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/AGENTS.md +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- package/scripts/tsconfig.json +34 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-meta-mailbox-state-write — deterministic gate for 0.11 Stage 0 step 3D-4
|
|
3
|
+
* commit2 (the cut). Renamed from check-meta-mailbox-dualwrite: after the cut the
|
|
4
|
+
* receipt is no longer DUAL-written (record.delivery is gone from the v2 record) —
|
|
5
|
+
* it lives SOLELY in the mailbox state store. A real temp sessions+mailbox dir; no
|
|
6
|
+
* backend, no network, no hook. Safe in the `pnpm check` static floor.
|
|
7
|
+
*
|
|
8
|
+
* Proves the post-cut contract (H1):
|
|
9
|
+
* - the citizen's meta-record FILE is byte-identical before/after enqueue AND read
|
|
10
|
+
* (enqueue/read no longer touch the record — invariant ⑤);
|
|
11
|
+
* - enqueue stamps state.lastEnqueuedAt, read stamps state.lastReadAt, and the
|
|
12
|
+
* enqueue receipt survives the read (field isolation on the state store);
|
|
13
|
+
* - lastDeliveredAt is never invented by read (doorbell owns delivery-time);
|
|
14
|
+
* - an empty inbox is a no-op on BOTH the record (untouched) and the state
|
|
15
|
+
* (state.json never created) — invariant ⑥;
|
|
16
|
+
* - a state-store drift makes the read throw fail-loud (partial failure surfaces).
|
|
17
|
+
*
|
|
18
|
+
* Scope is 3D-4 ONLY. The record is written as v2 by upsertMetaSession; enqueue/read
|
|
19
|
+
* read identity (dual-read) and mutate only the mailbox state.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import assert from "node:assert/strict";
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import os from "node:os";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import {
|
|
27
|
+
emptyMailboxReceiptState,
|
|
28
|
+
enqueueMetaMessage,
|
|
29
|
+
MetaRecordError,
|
|
30
|
+
mailboxReceiptStatePath,
|
|
31
|
+
readMailboxReceiptState,
|
|
32
|
+
readMetaInbox,
|
|
33
|
+
serializeMailboxReceiptState,
|
|
34
|
+
upsertMetaSession,
|
|
35
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
36
|
+
|
|
37
|
+
let passed = 0;
|
|
38
|
+
function ok(label: string, cond: boolean): void {
|
|
39
|
+
assert.ok(cond, label);
|
|
40
|
+
console.log(` ok ${label}`);
|
|
41
|
+
passed++;
|
|
42
|
+
}
|
|
43
|
+
function golden(label: string, actual: unknown, expected: unknown): void {
|
|
44
|
+
assert.deepStrictEqual(actual, expected, label);
|
|
45
|
+
console.log(` ok ${label}`);
|
|
46
|
+
passed++;
|
|
47
|
+
}
|
|
48
|
+
function throws(label: string, fn: () => unknown): void {
|
|
49
|
+
assert.throws(fn, MetaRecordError, label);
|
|
50
|
+
console.log(` ok ${label}`);
|
|
51
|
+
passed++;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ENQ = new Date("2026-03-01T12:00:00.000Z");
|
|
55
|
+
const ENQ_ISO = "2026-03-01T12:00:00.000Z";
|
|
56
|
+
const RD = new Date("2026-03-01T12:05:00.000Z");
|
|
57
|
+
const RD_ISO = "2026-03-01T12:05:00.000Z";
|
|
58
|
+
|
|
59
|
+
const tmpRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "psa-statewrite-")));
|
|
60
|
+
const sessionsDir = path.join(tmpRoot, "meta-sessions");
|
|
61
|
+
const mailboxDir = path.join(tmpRoot, "meta-mailbox");
|
|
62
|
+
|
|
63
|
+
// Seed a real v2 citizen via the upsert write path (the create that writes v2).
|
|
64
|
+
function seed(nativeSessionId: string): string {
|
|
65
|
+
const res = upsertMetaSession({
|
|
66
|
+
dir: sessionsDir,
|
|
67
|
+
input: { backend: "claude-code", nativeSessionId, transcriptPath: "/tmp/t.jsonl", cwd: "/tmp" },
|
|
68
|
+
now: ENQ,
|
|
69
|
+
});
|
|
70
|
+
return res.record.gardenId;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// --- enqueue → read: record FILE untouched, receipt lands in state only ----
|
|
75
|
+
const gidA = seed("n-state-a");
|
|
76
|
+
const recordFile = path.join(sessionsDir, `${gidA}.meta.json`);
|
|
77
|
+
const recordBytes0 = fs.readFileSync(recordFile, "utf8");
|
|
78
|
+
|
|
79
|
+
enqueueMetaMessage({ gardenId: gidA, body: "hello", sessionsDir, mailboxDir, now: ENQ });
|
|
80
|
+
ok(
|
|
81
|
+
"enqueue: the meta-record FILE is byte-identical (enqueue does not touch the record)",
|
|
82
|
+
fs.readFileSync(recordFile, "utf8") === recordBytes0,
|
|
83
|
+
);
|
|
84
|
+
const stateAfterEnq = readMailboxReceiptState({ gardenId: gidA, mailboxDir });
|
|
85
|
+
ok("enqueue: state.lastEnqueuedAt stamped (sole receipt home)", stateAfterEnq.lastEnqueuedAt === ENQ_ISO);
|
|
86
|
+
ok("enqueue: read-receipt not pre-stamped", stateAfterEnq.lastReadAt === null);
|
|
87
|
+
|
|
88
|
+
const readRes = readMetaInbox({ gardenId: gidA, sessionsDir, mailboxDir, now: RD });
|
|
89
|
+
ok("read: the queued message drained", readRes.messages.length === 1 && readRes.messages[0].body === "hello");
|
|
90
|
+
ok("read: result.readAt comes from the state stamp", readRes.readAt === RD_ISO);
|
|
91
|
+
ok(
|
|
92
|
+
"read: the meta-record FILE is STILL byte-identical (read does not touch the record)",
|
|
93
|
+
fs.readFileSync(recordFile, "utf8") === recordBytes0,
|
|
94
|
+
);
|
|
95
|
+
const stateAfterRead = readMailboxReceiptState({ gardenId: gidA, mailboxDir });
|
|
96
|
+
ok("read: state.lastReadAt stamped", stateAfterRead.lastReadAt === RD_ISO);
|
|
97
|
+
ok("read: enqueue receipt SURVIVES the read (field isolation)", stateAfterRead.lastEnqueuedAt === ENQ_ISO);
|
|
98
|
+
ok("read: lastDeliveredAt never invented by read (doorbell owns it)", stateAfterRead.lastDeliveredAt === null);
|
|
99
|
+
|
|
100
|
+
// --- empty inbox: no-op on BOTH record and state --------------------------
|
|
101
|
+
const gidB = seed("n-state-b");
|
|
102
|
+
const recBfile = path.join(sessionsDir, `${gidB}.meta.json`);
|
|
103
|
+
const recBytesB = fs.readFileSync(recBfile, "utf8");
|
|
104
|
+
const emptyRes = readMetaInbox({ gardenId: gidB, sessionsDir, mailboxDir, now: RD });
|
|
105
|
+
ok("empty inbox: nothing drained, readAt null", emptyRes.messages.length === 0 && emptyRes.readAt === null);
|
|
106
|
+
ok("empty inbox: record FILE untouched", fs.readFileSync(recBfile, "utf8") === recBytesB);
|
|
107
|
+
ok(
|
|
108
|
+
"empty inbox: state.json NEVER created (no receipt on the state either)",
|
|
109
|
+
!fs.existsSync(mailboxReceiptStatePath(mailboxDir, gidB)),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// --- partial-failure SURFACES (never swallowed) ---------------------------
|
|
113
|
+
const gidC = seed("n-state-c");
|
|
114
|
+
enqueueMetaMessage({ gardenId: gidC, body: "surface me", sessionsDir, mailboxDir, now: ENQ });
|
|
115
|
+
const OTHER = "20260301T120000-def456";
|
|
116
|
+
fs.writeFileSync(
|
|
117
|
+
mailboxReceiptStatePath(mailboxDir, gidC),
|
|
118
|
+
serializeMailboxReceiptState(emptyMailboxReceiptState(OTHER)),
|
|
119
|
+
);
|
|
120
|
+
throws("read: a state-store drift throws (partial failure surfaces, not swallowed)", () =>
|
|
121
|
+
readMetaInbox({ gardenId: gidC, sessionsDir, mailboxDir, now: RD }),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// a fresh citizen (no enqueue/read yet) has an all-null receipt state.
|
|
125
|
+
const gidD = seed("n-state-d");
|
|
126
|
+
golden(
|
|
127
|
+
"a never-touched citizen has an all-null receipt state",
|
|
128
|
+
readMailboxReceiptState({ gardenId: gidD, mailboxDir }),
|
|
129
|
+
emptyMailboxReceiptState(gidD),
|
|
130
|
+
);
|
|
131
|
+
} finally {
|
|
132
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(`[check-meta-mailbox-state-write] ${passed} assertions ok`);
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-meta-migration — deterministic gate for 0.11 Stage 0 step 3D-4 commit2:
|
|
3
|
+
* the v1→v2 delivery-receipt migration (migrateV1DeliveryReceipts) and its
|
|
4
|
+
* crash-order placement inside upsert. A real temp sessions+mailbox dir; no
|
|
5
|
+
* backend, no network, no hook. Safe in the `pnpm check` static floor.
|
|
6
|
+
*
|
|
7
|
+
* migrateV1DeliveryReceipts — per-field, STATE WINS, 3 timestamps only:
|
|
8
|
+
* - empty state + v1 receipts → fills every null field, writes, returns merged;
|
|
9
|
+
* - state already set on a field → v1 does NOT overwrite (state wins);
|
|
10
|
+
* - v1 all-null OR state already wins on every field v1 has → NO-OP: returns null,
|
|
11
|
+
* state.json never created ("migrating nothing is not a receipt", G4);
|
|
12
|
+
* - only the 3 timestamps move (wakeMode/deliveryLevel are capability, H2).
|
|
13
|
+
*
|
|
14
|
+
* crash-order (inside upsert): a v1 file's receipts migrate to state BEFORE the v2
|
|
15
|
+
* rewrite. Proven two ways: (a) a v1 record on disk → upsert attach lands the
|
|
16
|
+
* receipts in state AND rewrites the record to v2; (b) a drift'd state.json makes
|
|
17
|
+
* the migrate throw, and the record is STILL v1 (rewrite not reached) — so a crash
|
|
18
|
+
* there is recoverable (next attach re-migrates; state-wins is idempotent).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import os from "node:os";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import {
|
|
26
|
+
emptyMailboxReceiptState,
|
|
27
|
+
META_SCHEMA_VERSION,
|
|
28
|
+
META_SCHEMA_VERSION_V2,
|
|
29
|
+
MetaRecordError,
|
|
30
|
+
mailboxReceiptStatePath,
|
|
31
|
+
markEnqueued,
|
|
32
|
+
markRead,
|
|
33
|
+
migrateV1DeliveryReceipts,
|
|
34
|
+
mintMetaRecord,
|
|
35
|
+
parseMetaRecordAny,
|
|
36
|
+
readMailboxReceiptState,
|
|
37
|
+
serializeMailboxReceiptState,
|
|
38
|
+
serializeMetaRecord,
|
|
39
|
+
upsertMetaSession,
|
|
40
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
41
|
+
|
|
42
|
+
let passed = 0;
|
|
43
|
+
function ok(label: string, cond: boolean): void {
|
|
44
|
+
assert.ok(cond, label);
|
|
45
|
+
console.log(` ok ${label}`);
|
|
46
|
+
passed++;
|
|
47
|
+
}
|
|
48
|
+
function throws(label: string, fn: () => unknown): void {
|
|
49
|
+
assert.throws(fn, MetaRecordError, label);
|
|
50
|
+
console.log(` ok ${label}`);
|
|
51
|
+
passed++;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const GID = "20260301T120000-aaaaaa";
|
|
55
|
+
const E_ISO = "2026-03-01T12:00:00.000Z";
|
|
56
|
+
const R_ISO = "2026-03-01T12:05:00.000Z";
|
|
57
|
+
const tmpRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "psa-migrate-")));
|
|
58
|
+
const mailboxDir = path.join(tmpRoot, "mbx");
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// --- A: empty state + v1 receipts → fill all, write, create state.json -----
|
|
62
|
+
const merged = migrateV1DeliveryReceipts({
|
|
63
|
+
gardenId: GID,
|
|
64
|
+
delivery: { lastEnqueuedAt: E_ISO, lastDeliveredAt: null, lastReadAt: R_ISO },
|
|
65
|
+
mailboxDir,
|
|
66
|
+
});
|
|
67
|
+
ok(
|
|
68
|
+
"migrate: empty state + v1 receipts fills null fields, returns merged",
|
|
69
|
+
merged !== null &&
|
|
70
|
+
merged.lastEnqueuedAt === E_ISO &&
|
|
71
|
+
merged.lastReadAt === R_ISO &&
|
|
72
|
+
merged.lastDeliveredAt === null,
|
|
73
|
+
);
|
|
74
|
+
ok("migrate: state.json created on a non-empty migration", fs.existsSync(mailboxReceiptStatePath(mailboxDir, GID)));
|
|
75
|
+
|
|
76
|
+
// --- B: state wins — a re-migrate with DIFFERENT v1 values changes nothing --
|
|
77
|
+
const noop = migrateV1DeliveryReceipts({
|
|
78
|
+
gardenId: GID,
|
|
79
|
+
delivery: {
|
|
80
|
+
lastEnqueuedAt: "2099-01-01T00:00:00.000Z",
|
|
81
|
+
lastDeliveredAt: null,
|
|
82
|
+
lastReadAt: "2099-01-01T00:00:00.000Z",
|
|
83
|
+
},
|
|
84
|
+
mailboxDir,
|
|
85
|
+
});
|
|
86
|
+
ok("migrate: state wins — re-migrate over set fields is a no-op (returns null)", noop === null);
|
|
87
|
+
const after = readMailboxReceiptState({ gardenId: GID, mailboxDir });
|
|
88
|
+
ok(
|
|
89
|
+
"migrate: state values unchanged by the losing re-migrate",
|
|
90
|
+
after.lastEnqueuedAt === E_ISO && after.lastReadAt === R_ISO,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// --- C: partial fill — v1 fills only the null field, state keeps the rest ----
|
|
94
|
+
const GID2 = "20260301T120000-bbbbbb";
|
|
95
|
+
migrateV1DeliveryReceipts({
|
|
96
|
+
gardenId: GID2,
|
|
97
|
+
delivery: { lastEnqueuedAt: E_ISO, lastDeliveredAt: null, lastReadAt: null },
|
|
98
|
+
mailboxDir,
|
|
99
|
+
});
|
|
100
|
+
const partial = migrateV1DeliveryReceipts({
|
|
101
|
+
gardenId: GID2,
|
|
102
|
+
delivery: { lastEnqueuedAt: "2099-01-01T00:00:00.000Z", lastDeliveredAt: null, lastReadAt: R_ISO },
|
|
103
|
+
mailboxDir,
|
|
104
|
+
});
|
|
105
|
+
ok(
|
|
106
|
+
"migrate: partial — fills the still-null lastReadAt, keeps the won lastEnqueuedAt",
|
|
107
|
+
partial !== null && partial.lastEnqueuedAt === E_ISO && partial.lastReadAt === R_ISO,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// --- D: v1 all-null → no-op, NO state.json created --------------------------
|
|
111
|
+
const GID3 = "20260301T120000-cccccc";
|
|
112
|
+
const none = migrateV1DeliveryReceipts({
|
|
113
|
+
gardenId: GID3,
|
|
114
|
+
delivery: { lastEnqueuedAt: null, lastDeliveredAt: null, lastReadAt: null },
|
|
115
|
+
mailboxDir,
|
|
116
|
+
});
|
|
117
|
+
ok("migrate: v1 all-null is a no-op (returns null)", none === null);
|
|
118
|
+
ok("migrate: v1 all-null never creates state.json", !fs.existsSync(mailboxReceiptStatePath(mailboxDir, GID3)));
|
|
119
|
+
|
|
120
|
+
// --- E: crash-order via upsert — v1 record attaches → receipts migrate + v2 -
|
|
121
|
+
const sessionsDir = path.join(tmpRoot, "sessions-e");
|
|
122
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
123
|
+
const mbxE = path.join(tmpRoot, "mbx-e");
|
|
124
|
+
const v1 = markRead(
|
|
125
|
+
markEnqueued(
|
|
126
|
+
mintMetaRecord(
|
|
127
|
+
{ backend: "claude-code", nativeSessionId: "n-mig", transcriptPath: "/t.jsonl", cwd: "/c" },
|
|
128
|
+
new Date(E_ISO),
|
|
129
|
+
),
|
|
130
|
+
new Date(E_ISO),
|
|
131
|
+
),
|
|
132
|
+
new Date(R_ISO),
|
|
133
|
+
);
|
|
134
|
+
fs.writeFileSync(path.join(sessionsDir, `${v1.gardenId}.meta.json`), serializeMetaRecord(v1));
|
|
135
|
+
const res = upsertMetaSession({
|
|
136
|
+
dir: sessionsDir,
|
|
137
|
+
mailboxDir: mbxE,
|
|
138
|
+
input: { backend: "claude-code", nativeSessionId: "n-mig", cwd: "/c" },
|
|
139
|
+
now: new Date(R_ISO),
|
|
140
|
+
});
|
|
141
|
+
ok("crash-order: upsert attach rewrote the record to v2", res.record.schemaVersion === META_SCHEMA_VERSION_V2);
|
|
142
|
+
const migrated = readMailboxReceiptState({ gardenId: v1.gardenId, mailboxDir: mbxE });
|
|
143
|
+
ok(
|
|
144
|
+
"crash-order: the v1 record's receipts migrated to state BEFORE the v2 rewrite",
|
|
145
|
+
migrated.lastEnqueuedAt === E_ISO && migrated.lastReadAt === R_ISO,
|
|
146
|
+
);
|
|
147
|
+
const onDisk = parseMetaRecordAny(fs.readFileSync(path.join(sessionsDir, `${v1.gardenId}.meta.json`), "utf8"));
|
|
148
|
+
ok("crash-order: on-disk record is v2 (delivery gone)", onDisk.schemaVersion === META_SCHEMA_VERSION_V2);
|
|
149
|
+
|
|
150
|
+
// --- F: crash-order safety — a drift'd state makes migrate throw, record stays v1
|
|
151
|
+
const sessionsDir2 = path.join(tmpRoot, "sessions-f");
|
|
152
|
+
fs.mkdirSync(sessionsDir2, { recursive: true });
|
|
153
|
+
const mbxF = path.join(tmpRoot, "mbx-f");
|
|
154
|
+
const v1b = markEnqueued(
|
|
155
|
+
mintMetaRecord(
|
|
156
|
+
{ backend: "claude-code", nativeSessionId: "n-drift", transcriptPath: "/t.jsonl", cwd: "/c" },
|
|
157
|
+
new Date(E_ISO),
|
|
158
|
+
),
|
|
159
|
+
new Date(E_ISO),
|
|
160
|
+
);
|
|
161
|
+
fs.writeFileSync(path.join(sessionsDir2, `${v1b.gardenId}.meta.json`), serializeMetaRecord(v1b));
|
|
162
|
+
// plant a drift'd state.json (body claims a DIFFERENT citizen) under v1b's gardenId
|
|
163
|
+
fs.mkdirSync(path.dirname(mailboxReceiptStatePath(mbxF, v1b.gardenId)), { recursive: true });
|
|
164
|
+
fs.writeFileSync(
|
|
165
|
+
mailboxReceiptStatePath(mbxF, v1b.gardenId),
|
|
166
|
+
serializeMailboxReceiptState(emptyMailboxReceiptState("20260301T120000-dddddd")),
|
|
167
|
+
);
|
|
168
|
+
throws("crash-order safety: a drift'd state makes the migrate (hence upsert) throw", () =>
|
|
169
|
+
upsertMetaSession({
|
|
170
|
+
dir: sessionsDir2,
|
|
171
|
+
mailboxDir: mbxF,
|
|
172
|
+
input: { backend: "claude-code", nativeSessionId: "n-drift", cwd: "/c" },
|
|
173
|
+
now: new Date(R_ISO),
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
const stillV1 = parseMetaRecordAny(fs.readFileSync(path.join(sessionsDir2, `${v1b.gardenId}.meta.json`), "utf8"));
|
|
177
|
+
ok(
|
|
178
|
+
"crash-order safety: record is STILL v1 (v2 rewrite not reached → next attach re-migrates)",
|
|
179
|
+
stillV1.schemaVersion === META_SCHEMA_VERSION,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// --- G: create + attach v2→v2 do NOT migrate (no state.json) ---------------
|
|
183
|
+
// A fresh create writes v2 with no v1 delivery to migrate; a later attach over the
|
|
184
|
+
// now-v2 record sees raw.schemaVersion===v2, so the migration branch never runs.
|
|
185
|
+
const sessionsDir3 = path.join(tmpRoot, "sessions-g");
|
|
186
|
+
fs.mkdirSync(sessionsDir3, { recursive: true });
|
|
187
|
+
const mbxG = path.join(tmpRoot, "mbx-g");
|
|
188
|
+
const created = upsertMetaSession({
|
|
189
|
+
dir: sessionsDir3,
|
|
190
|
+
mailboxDir: mbxG,
|
|
191
|
+
input: { backend: "claude-code", nativeSessionId: "n-fresh", cwd: "/c" },
|
|
192
|
+
now: new Date(E_ISO),
|
|
193
|
+
});
|
|
194
|
+
ok(
|
|
195
|
+
"create: a fresh v2 create does not migrate (state.json not created)",
|
|
196
|
+
!fs.existsSync(mailboxReceiptStatePath(mbxG, created.record.gardenId)),
|
|
197
|
+
);
|
|
198
|
+
upsertMetaSession({
|
|
199
|
+
dir: sessionsDir3,
|
|
200
|
+
mailboxDir: mbxG,
|
|
201
|
+
input: { backend: "claude-code", nativeSessionId: "n-fresh", cwd: "/c2" },
|
|
202
|
+
now: new Date(R_ISO),
|
|
203
|
+
});
|
|
204
|
+
ok(
|
|
205
|
+
"attach v2→v2: no migration (raw is v2, state.json still absent)",
|
|
206
|
+
!fs.existsSync(mailboxReceiptStatePath(mbxG, created.record.gardenId)),
|
|
207
|
+
);
|
|
208
|
+
} finally {
|
|
209
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log(`[check-meta-migration] ${passed} assertions ok`);
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-meta-receiver-marker — deterministic gate for the meta-receiver presence
|
|
3
|
+
* marker (SE-2 slice 2b). The active-receiver signal a self-fetch backend (Claude
|
|
4
|
+
* Code) needs: a meta-record proves a session once existed; this marker proves a
|
|
5
|
+
* live watch owner is still there to be woken. Without it a terminated session's
|
|
6
|
+
* lingering record reads as a ghost active receiver and replies pile up as mailbox
|
|
7
|
+
* garbage.
|
|
8
|
+
*
|
|
9
|
+
* Proves:
|
|
10
|
+
* - write→read round-trip preserves every field; keyed by GARDEN id (not owner pid),
|
|
11
|
+
* because deliverability lookup starts from a target garden id.
|
|
12
|
+
* - atomic write lands valid JSON at 0600.
|
|
13
|
+
* - dead-owner / pid-reuse guard: a marker whose ownerStartKey no longer matches the
|
|
14
|
+
* live owner reads as null (inactive), distinct from "no marker". verifyOwner:false
|
|
15
|
+
* bypasses it for inspection.
|
|
16
|
+
* - armProvenance is constrained to the arm-capable events; "user-prompt-submit" (and
|
|
17
|
+
* any other value) is rejected at write — UserPromptSubmit can never mint a presence
|
|
18
|
+
* it cannot back.
|
|
19
|
+
* - record-backing is NOT checked by the reader (recordBacked is the deliverability
|
|
20
|
+
* predicate's explicit fact, so an absent record and a dead owner stay distinct).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import assert from "node:assert/strict";
|
|
24
|
+
import { mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { tmpdir } from "node:os";
|
|
26
|
+
import * as path from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
import {
|
|
29
|
+
META_RECEIVER_ARM_PROVENANCES,
|
|
30
|
+
metaReceiverMarkerPath,
|
|
31
|
+
processStartKey,
|
|
32
|
+
readMetaReceiverMarker,
|
|
33
|
+
writeMetaReceiverMarker,
|
|
34
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
35
|
+
|
|
36
|
+
let passed = 0;
|
|
37
|
+
function ok(label: string, cond: boolean): void {
|
|
38
|
+
assert.ok(cond, label);
|
|
39
|
+
console.log(` ok ${label}`);
|
|
40
|
+
passed++;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
44
|
+
const DIR = mkdtempSync(path.join(tmpdir(), "psa-meta-receivers-"));
|
|
45
|
+
const GARDEN = "20260614T120000-aaaaaa";
|
|
46
|
+
|
|
47
|
+
// ── write → read round-trip, garden-id keyed ────────────────────────────────
|
|
48
|
+
const file = writeMetaReceiverMarker({
|
|
49
|
+
gardenId: GARDEN,
|
|
50
|
+
backend: "claude-code",
|
|
51
|
+
nativeSessionId: "n-recv-a",
|
|
52
|
+
ownerPid: process.pid, // this live node process — start-key will match on read
|
|
53
|
+
armProvenance: "session-start",
|
|
54
|
+
receiversDir: DIR,
|
|
55
|
+
});
|
|
56
|
+
ok(
|
|
57
|
+
"marker path is keyed by garden id",
|
|
58
|
+
file === metaReceiverMarkerPath(GARDEN, DIR) && file.endsWith(`${GARDEN}.json`),
|
|
59
|
+
);
|
|
60
|
+
ok("write lands JSON at mode 0600", (statSync(file).mode & 0o777) === 0o600);
|
|
61
|
+
|
|
62
|
+
const back = readMetaReceiverMarker({ gardenId: GARDEN, receiversDir: DIR });
|
|
63
|
+
ok("read returns a marker for a live owner", back !== null);
|
|
64
|
+
if (back) {
|
|
65
|
+
ok("round-trip gardenId", back.gardenId === GARDEN);
|
|
66
|
+
ok("round-trip backend", back.backend === "claude-code");
|
|
67
|
+
ok("round-trip nativeSessionId", back.nativeSessionId === "n-recv-a");
|
|
68
|
+
ok("round-trip ownerPid", back.ownerPid === process.pid);
|
|
69
|
+
ok("ownerStartKey computed at write equals the live owner key", back.ownerStartKey === processStartKey(process.pid));
|
|
70
|
+
ok("ownerKind defaults to claude-code-cli", back.ownerKind === "claude-code-cli");
|
|
71
|
+
ok("round-trip armProvenance", back.armProvenance === "session-start");
|
|
72
|
+
ok("updatedAt present", typeof back.updatedAt === "string" && back.updatedAt.length > 0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── dead-owner / pid-reuse guard ────────────────────────────────────────────
|
|
76
|
+
// A marker whose start-key no longer matches the live pid must read as null.
|
|
77
|
+
const ghostGarden = "20260614T120000-bbbbbb";
|
|
78
|
+
const ghostFile = metaReceiverMarkerPath(ghostGarden, DIR);
|
|
79
|
+
writeFileSync(
|
|
80
|
+
ghostFile,
|
|
81
|
+
`${JSON.stringify({
|
|
82
|
+
gardenId: ghostGarden,
|
|
83
|
+
backend: "claude-code",
|
|
84
|
+
nativeSessionId: "n-ghost",
|
|
85
|
+
ownerPid: process.pid,
|
|
86
|
+
ownerStartKey: "linux:1", // bogus start-key for this pid → guard must reject
|
|
87
|
+
ownerKind: "claude-code-cli",
|
|
88
|
+
armProvenance: "session-start",
|
|
89
|
+
updatedAt: "2026-06-14T03:00:00.000Z",
|
|
90
|
+
})}\n`,
|
|
91
|
+
);
|
|
92
|
+
ok(
|
|
93
|
+
"dead-owner (start-key mismatch) marker reads as null",
|
|
94
|
+
readMetaReceiverMarker({ gardenId: ghostGarden, receiversDir: DIR }) === null,
|
|
95
|
+
);
|
|
96
|
+
ok(
|
|
97
|
+
"verifyOwner:false bypasses the guard (inspection only)",
|
|
98
|
+
readMetaReceiverMarker({ gardenId: ghostGarden, receiversDir: DIR, verifyOwner: false }) !== null,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// ── armProvenance constraint ────────────────────────────────────────────────
|
|
102
|
+
ok(
|
|
103
|
+
"arm provenances are exactly the arm-capable events",
|
|
104
|
+
JSON.stringify([...META_RECEIVER_ARM_PROVENANCES].sort()) ===
|
|
105
|
+
JSON.stringify(["cwd-changed", "file-changed", "session-start"]),
|
|
106
|
+
);
|
|
107
|
+
let upsRejected = false;
|
|
108
|
+
try {
|
|
109
|
+
writeMetaReceiverMarker({
|
|
110
|
+
gardenId: GARDEN,
|
|
111
|
+
backend: "claude-code",
|
|
112
|
+
nativeSessionId: "n-recv-a",
|
|
113
|
+
ownerPid: process.pid,
|
|
114
|
+
// @ts-expect-error — user-prompt-submit is intentionally NOT a valid arm provenance
|
|
115
|
+
armProvenance: "user-prompt-submit",
|
|
116
|
+
receiversDir: DIR,
|
|
117
|
+
});
|
|
118
|
+
} catch {
|
|
119
|
+
upsRejected = true;
|
|
120
|
+
}
|
|
121
|
+
ok("user-prompt-submit armProvenance is rejected at write (UPS cannot mint presence)", upsRejected);
|
|
122
|
+
|
|
123
|
+
// ── absent marker, no record-backing requirement ────────────────────────────
|
|
124
|
+
ok(
|
|
125
|
+
"absent marker reads as null",
|
|
126
|
+
readMetaReceiverMarker({ gardenId: "20260614T120000-cccccc", receiversDir: DIR }) === null,
|
|
127
|
+
);
|
|
128
|
+
// The round-trip marker above has NO backing meta-record in this dir, yet it read
|
|
129
|
+
// fine — the reader does not gate on record existence (that is the predicate's fact).
|
|
130
|
+
ok("reader does not require a backing record (recordBacked is the predicate's job)", back !== null);
|
|
131
|
+
|
|
132
|
+
// corrupt file → null, never throw.
|
|
133
|
+
const corruptGarden = "20260614T120000-dddddd";
|
|
134
|
+
writeFileSync(metaReceiverMarkerPath(corruptGarden, DIR), "{ not json");
|
|
135
|
+
ok(
|
|
136
|
+
"corrupt marker reads as null (no throw)",
|
|
137
|
+
readMetaReceiverMarker({ gardenId: corruptGarden, receiversDir: DIR }) === null,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// ── HOOK WIRING source guard: the marker is only as honest as where it is written ──
|
|
141
|
+
const hookSrc = readFileSync(path.join(REPO_DIR, "pi-extensions", "meta-bridge-hook.ts"), "utf8");
|
|
142
|
+
|
|
143
|
+
ok("hook imports + calls writeMetaReceiverMarker", /writeMetaReceiverMarker/.test(hookSrc));
|
|
144
|
+
|
|
145
|
+
// ownerPid is process.ppid ONLY — never the grandparent (parentPid). The receiver
|
|
146
|
+
// owner is the watchPaths subscriber (the Claude CLI); a grandparent (login shell)
|
|
147
|
+
// outlives the session and would resurrect the ghost active-receiver leak.
|
|
148
|
+
ok("receiver marker ownerPid is process.ppid", /ownerPid\s*=\s*process\.ppid/.test(hookSrc));
|
|
149
|
+
ok("hook does NOT use parentPid (no grandparent owner)", !/parentPid\s*\(/.test(hookSrc));
|
|
150
|
+
|
|
151
|
+
// UserPromptSubmit must early-return BEFORE the receiver marker write — it cannot
|
|
152
|
+
// arm a watch, so it must never mint a presence. Check the early-return appears
|
|
153
|
+
// before the writeMetaReceiverMarker call site in source order.
|
|
154
|
+
const upsReturnAt = hookSrc.search(/eventName\s*===\s*"UserPromptSubmit"/);
|
|
155
|
+
const recvWriteAt = hookSrc.search(/writeMetaReceiverMarker\s*\(/);
|
|
156
|
+
ok(
|
|
157
|
+
"UserPromptSubmit early-return precedes the receiver marker write",
|
|
158
|
+
upsReturnAt >= 0 && recvWriteAt >= 0 && upsReturnAt < recvWriteAt,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// armProvenanceFor maps ONLY the arm-capable events and returns null otherwise — an
|
|
162
|
+
// unknown event must not masquerade as an optimistic session-start.
|
|
163
|
+
const armFnMatch = hookSrc.match(/function armProvenanceFor\([^)]*\)[^{]*\{[\s\S]*?\n\}/);
|
|
164
|
+
ok("armProvenanceFor is present", armFnMatch !== null);
|
|
165
|
+
if (armFnMatch) {
|
|
166
|
+
const body = armFnMatch[0];
|
|
167
|
+
ok(
|
|
168
|
+
"armProvenanceFor maps SessionStart/CwdChanged/FileChanged",
|
|
169
|
+
/SessionStart/.test(body) && /CwdChanged/.test(body) && /FileChanged/.test(body),
|
|
170
|
+
);
|
|
171
|
+
ok(
|
|
172
|
+
"armProvenanceFor returns null for unknown events (fail-closed, no optimistic session-start)",
|
|
173
|
+
/return null/.test(body),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The receiver marker write rides the watch-arm block (near the inbox.signal arm),
|
|
178
|
+
// not the sender-marker block — keep the two presences from drifting together.
|
|
179
|
+
const signalAt = hookSrc.search(/inbox\.signal/);
|
|
180
|
+
ok(
|
|
181
|
+
"receiver marker write sits within the watch-arm region (near inbox.signal)",
|
|
182
|
+
signalAt >= 0 && recvWriteAt > signalAt,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
console.log(`\ncheck-meta-receiver-marker: ${passed} checks passed`);
|