@nanobpm/nano-workforce 0.183.1 → 0.184.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/CHANGELOG.md +12 -0
- package/app/agentic/agent-history.test.ts +165 -0
- package/app/agentic/agent-history.ts +211 -0
- package/app/agentic/claim-registry.test.ts +3 -2
- package/app/agentic/claim-registry.ts +8 -6
- package/app/agentic/cockpit/agent-history-render.test.ts +85 -0
- package/app/agentic/cockpit/agent-history-render.ts +168 -0
- package/app/agentic/cockpit/agent-history-view.test.ts +104 -0
- package/app/agentic/cockpit/agent-history-view.ts +186 -0
- package/app/agentic/cockpit/index.ts +21 -0
- package/app/agentic/cockpit/mount.test.ts +133 -1
- package/app/agentic/cockpit/supply-boot-agent-history.test.ts +144 -0
- package/app/agentic/cockpit/supply-boot.ts +134 -1
- package/app/agentic/cockpit/supply-view.ts +3 -3
- package/app/agentic/cockpit/transcript-view.ts +1 -1
- package/app/agentic/correlation-store.test.ts +14 -10
- package/app/agentic/correlation-store.ts +4 -3
- package/app/agentic/correlation.test.ts +24 -16
- package/app/agentic/correlation.ts +25 -12
- package/app/agentic/families/claim.family.test.ts +2 -1
- package/app/agentic/families/relay.family.test.ts +74 -73
- package/app/agentic/families/relay.family.ts +24 -21
- package/app/agentic/transcript-read.test.ts +108 -17
- package/app/agentic/transcript-read.ts +41 -7
- package/app/contracts.ts +10 -2
- package/app/mcpToolSurface.ts +8 -1
- package/db/migrations/101_agentic_history_read_expand.sql +34 -0
- package/openapi.yaml +360 -0
- package/operations/agentHistoryEndpoints.test.ts +106 -0
- package/operations/getAgentInstanceHistory.ts +37 -0
- package/operations/getAgenticSupply.test.ts +41 -2
- package/operations/getAgenticSupply.ts +7 -5
- package/operations/getAgenticTranscript.test.ts +5 -4
- package/operations/listAgentInstances.ts +42 -0
- package/operations/listAgenticTranscripts.test.ts +10 -9
- package/package.json +3 -3
- package/pages/cockpit/cockpit.css +104 -0
- package/pages/cockpit/mount.js +286 -3
- package/test/agentic-e2e.test.ts +4 -1
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
// asks for, so a worker's current jobKeys light up in the supply feed / cockpit.
|
|
13
13
|
// - `resolve(jobKey)` gives the cockpit the process-instance / plan a terminal belongs to, so the
|
|
14
14
|
// drilled bytes line up with "that process instance / this plan".
|
|
15
|
-
// - the relay terminal for a job is the
|
|
16
|
-
//
|
|
17
|
-
//
|
|
15
|
+
// - the relay terminal for a job is the instance-scoped stream `composeStreamId(instance, jobKey)`
|
|
16
|
+
// from `@nanobpm/agentic/emit` (issue #738) — the ONE codec the producer also writes with, so the
|
|
17
|
+
// report can repoint a worker's drill stream at the exact live stream the producer emits without a
|
|
18
|
+
// second, divergent naming scheme.
|
|
18
19
|
//
|
|
19
20
|
// Who populates it: the orchestrator that dispatches an agentic job to a worker (it holds the whole
|
|
20
21
|
// job payload — jobKey, processInstanceKey, bpmnProcessId, and the plan/epic it belongs to) calls
|
|
@@ -26,20 +27,32 @@
|
|
|
26
27
|
// is untouched — correlation is an app-side observation, not a new wire type; ADVISORY — it is a
|
|
27
28
|
// read-only join for visibility and NEVER hard-locks or gates a BPMN sequence flow.
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
import { composeStreamId } from "@nanobpm/agentic/emit";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The relay-stream prefix for a jobKey-scoped terminal stream. Retained ONLY for the two surfaces
|
|
34
|
+
* that still address a job by a bare, slash-free stream id — the Explorer Stage-0 transcript URL
|
|
35
|
+
* (`app/agentic/transcript-url.ts`, #543) and the permission control-lane resolution default
|
|
36
|
+
* (`app/agentic/permission-bridge.ts`). The cockpit transcript DATA plane no longer uses it: a job
|
|
37
|
+
* transcript stream is the instance-scoped `composeStreamId(instance, jobKey)` (issue #738), so that
|
|
38
|
+
* producer and consumer share ONE codec (`@nanobpm/agentic/emit`) and never diverge again.
|
|
39
|
+
*/
|
|
30
40
|
export const JOB_STREAM_PREFIX = "job:";
|
|
31
41
|
|
|
32
|
-
/** The
|
|
42
|
+
/** The bare, slash-free jobKey-scoped stream id `job:<jobKey>` — see {@link JOB_STREAM_PREFIX}. */
|
|
33
43
|
export function jobStream(jobKey: string): string {
|
|
34
44
|
return `${JOB_STREAM_PREFIX}${jobKey}`;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
/**
|
|
38
|
-
* The jobKey encoded in a jobKey
|
|
39
|
-
*
|
|
40
|
-
*
|
|
48
|
+
* The jobKey encoded in a bare `job:<jobKey>` alias — the Stage-0 transcript URL / permission-lane
|
|
49
|
+
* scheme retained by {@link JOB_STREAM_PREFIX}. Returns undefined for ANY other stream, including an
|
|
50
|
+
* instance-scoped `composeStreamId(instance, jobKey)` data-plane id (decode THAT with `parseStreamId`)
|
|
51
|
+
* and a bare `job:` with no jobKey — keeping the "empty jobKey is invalid" invariant (`link()` ignores
|
|
52
|
+
* empty jobKeys) consistent for callers. This is the READ-path alias decoder ONLY: the write/relay side
|
|
53
|
+
* addresses jobs solely by the instance-scoped id, so it must never route through this.
|
|
41
54
|
*/
|
|
42
|
-
export function
|
|
55
|
+
export function jobKeyOfJobStream(stream: string): string | undefined {
|
|
43
56
|
if (!stream.startsWith(JOB_STREAM_PREFIX)) return undefined;
|
|
44
57
|
const jobKey = stream.slice(JOB_STREAM_PREFIX.length);
|
|
45
58
|
return jobKey === "" ? undefined : jobKey;
|
|
@@ -64,7 +77,7 @@ export interface JobCorrelation {
|
|
|
64
77
|
readonly elementInstanceKey?: string;
|
|
65
78
|
/** The plan / epic key this job is part of (e.g. `owner/repo#142`), if known. */
|
|
66
79
|
readonly planKey?: string;
|
|
67
|
-
/** The relay stream id the job's terminal is relayed on (`
|
|
80
|
+
/** The relay stream id the job's terminal is relayed on (`composeStreamId(instance, jobKey)`). */
|
|
68
81
|
readonly stream: string;
|
|
69
82
|
}
|
|
70
83
|
|
|
@@ -120,7 +133,7 @@ export class CorrelationRegistry {
|
|
|
120
133
|
this.#context.set(jobKey, {
|
|
121
134
|
...existing,
|
|
122
135
|
jobKey,
|
|
123
|
-
stream:
|
|
136
|
+
stream: composeStreamId(instance, jobKey),
|
|
124
137
|
...stripUndefined(context),
|
|
125
138
|
});
|
|
126
139
|
}
|
|
@@ -183,7 +196,7 @@ export class CorrelationRegistry {
|
|
|
183
196
|
*/
|
|
184
197
|
primaryStreamFor(instance: string): string | undefined {
|
|
185
198
|
const [first] = this.jobKeysFor(instance);
|
|
186
|
-
return first === undefined ? undefined :
|
|
199
|
+
return first === undefined ? undefined : composeStreamId(instance, first);
|
|
187
200
|
}
|
|
188
201
|
|
|
189
202
|
/** The number of currently-linked jobs. */
|
|
@@ -12,6 +12,7 @@ import { AgenticHub } from "@nanobpm/agentic/channel";
|
|
|
12
12
|
import type { Authenticator, ChannelConnection, ChannelTransport } from "@nanobpm/agentic/channel";
|
|
13
13
|
import { encodeFrame, type Frame } from "@nanobpm/agentic/protocol";
|
|
14
14
|
import { assert, assertEquals } from "#test-assert";
|
|
15
|
+
import { composeStreamId } from "@nanobpm/agentic/emit";
|
|
15
16
|
import { noopLog } from "../../../test/log.ts";
|
|
16
17
|
import { currentClaimRegistry } from "../claim-registry.ts";
|
|
17
18
|
import type { AgenticContext } from "../registry.ts";
|
|
@@ -91,7 +92,7 @@ test("a claim populates the worker's jobKeys with ZERO transcript (visibility no
|
|
|
91
92
|
const reg = currentClaimRegistry();
|
|
92
93
|
assert(reg, "registry mounted");
|
|
93
94
|
assertEquals(reg.jobKeysFor("wk-a"), ["8420"]);
|
|
94
|
-
assertEquals(reg.primaryStreamFor("wk-a"), "
|
|
95
|
+
assertEquals(reg.primaryStreamFor("wk-a"), composeStreamId("wk-a", "8420"), "the drill stream repoints at the claimed job");
|
|
95
96
|
} finally {
|
|
96
97
|
family.teardown?.();
|
|
97
98
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
TRANSCRIPT_SCHEMA_SQL,
|
|
25
25
|
} from "@nanobpm/agentic/transcript";
|
|
26
26
|
import { assert, assertEquals, assertThrows } from "#test-assert";
|
|
27
|
+
import { composeStreamId } from "@nanobpm/agentic/emit";
|
|
27
28
|
import { noopLog } from "../../../test/log.ts";
|
|
28
29
|
import { CorrelationRegistry, jobStream } from "../correlation.ts";
|
|
29
30
|
import { AgenticCorrelationStore } from "../correlation-store.ts";
|
|
@@ -349,15 +350,15 @@ test("H6 correlation write-side: a produce on job:<k> links instance→[k]; stre
|
|
|
349
350
|
const { service, hub } = mkCorrelatedService(registry, memoryDb(), correlation, byConnection);
|
|
350
351
|
const p = connect("prod", registry);
|
|
351
352
|
|
|
352
|
-
// The first `produce` for
|
|
353
|
+
// The first `produce` for an instance-scoped stream links the producing worker instance → jobKey, from
|
|
353
354
|
// data already crossing the wire (jobKey decoded from the stream id; instance from the connection).
|
|
354
|
-
hub.handler?.(produce(
|
|
355
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "chunk"), p.conn);
|
|
355
356
|
assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "instance → [jobKey] is now linked");
|
|
356
|
-
assertEquals(correlation.resolve("k1")?.stream,
|
|
357
|
+
assertEquals(correlation.resolve("k1")?.stream, composeStreamId("worker-A", "k1"), "context carries the instance-scoped stream");
|
|
357
358
|
assertEquals(correlation.count(), 1);
|
|
358
359
|
|
|
359
360
|
// Job end (stream completion) releases the correlation, so the worker's supply row clears it.
|
|
360
|
-
service.completeStream(
|
|
361
|
+
service.completeStream(composeStreamId("worker-A", "k1"));
|
|
361
362
|
assertEquals(correlation.jobKeysFor("worker-A"), [], "completion releases the job");
|
|
362
363
|
assertEquals(correlation.resolve("k1"), undefined);
|
|
363
364
|
assertEquals(correlation.count(), 0);
|
|
@@ -373,10 +374,10 @@ test("H6 correlation write-side: an unpersisted stream completion releases its j
|
|
|
373
374
|
// producer — otherwise #reconcile keeps re-completing the stream on every subsequent frame.
|
|
374
375
|
const { service, hub } = mkCorrelatedService(registry, undefined, correlation, byConnection);
|
|
375
376
|
const p = connect("prod", registry);
|
|
376
|
-
hub.handler?.(produce(
|
|
377
|
+
hub.handler?.(produce(composeStreamId("worker-U", "kU"), 1, "x"), p.conn);
|
|
377
378
|
assertEquals(correlation.jobKeysFor("worker-U"), ["kU"]);
|
|
378
379
|
|
|
379
|
-
service.completeStream(
|
|
380
|
+
service.completeStream(composeStreamId("worker-U", "kU"));
|
|
380
381
|
assertEquals(correlation.jobKeysFor("worker-U"), [], "unpersisted completion releases the job");
|
|
381
382
|
assertEquals(correlation.count(), 0);
|
|
382
383
|
|
|
@@ -393,16 +394,16 @@ test("H6 correlation write-side: a late produce after completion does not resurr
|
|
|
393
394
|
const byConnection = new Map([["prod", "worker-R"]]);
|
|
394
395
|
const { service, hub } = mkCorrelatedService(registry, undefined, correlation, byConnection);
|
|
395
396
|
const p = connect("prod", registry);
|
|
396
|
-
hub.handler?.(produce(
|
|
397
|
+
hub.handler?.(produce(composeStreamId("worker-R", "kR"), 1, "x"), p.conn);
|
|
397
398
|
assertEquals(correlation.jobKeysFor("worker-R"), ["kR"]);
|
|
398
399
|
|
|
399
400
|
// Job end completes the stream and releases the correlation.
|
|
400
|
-
service.completeStream(
|
|
401
|
+
service.completeStream(composeStreamId("worker-R", "kR"));
|
|
401
402
|
assertEquals(correlation.count(), 0, "completion releases the job");
|
|
402
403
|
|
|
403
404
|
// A late `produce` frame for the same, still-live producer must NOT re-link/re-own the completed
|
|
404
405
|
// stream — otherwise it resurrects a jobKey after it was released.
|
|
405
|
-
hub.handler?.(produce(
|
|
406
|
+
hub.handler?.(produce(composeStreamId("worker-R", "kR"), 1, "late"), p.conn);
|
|
406
407
|
assertEquals(correlation.count(), 0, "a late produce does not resurrect a completed stream");
|
|
407
408
|
assertEquals(correlation.jobKeysFor("worker-R"), [], "released jobKey stays released after a late produce");
|
|
408
409
|
service.teardown();
|
|
@@ -420,20 +421,20 @@ test("H6 correlation write-side: a worker starting a NEW job over its live conne
|
|
|
420
421
|
const p = connect("prod", registry);
|
|
421
422
|
|
|
422
423
|
// Job 1: the worker relays k1's terminal → linked.
|
|
423
|
-
hub.handler?.(produce(
|
|
424
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
|
|
424
425
|
assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "job 1 is the current job");
|
|
425
426
|
|
|
426
427
|
// Job 2 begins on the SAME live connection (no disconnect). The worker relaying k2's terminal PROVES
|
|
427
428
|
// k1 finished (one job at a time) → k1 is superseded: released AND flushed to a durable past session.
|
|
428
|
-
hub.handler?.(produce(
|
|
429
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k2"), 1, "job-2 line"), p.conn);
|
|
429
430
|
assertEquals(correlation.jobKeysFor("worker-A"), ["k2"], "the supply row shows ONLY the current job — no accumulation");
|
|
430
431
|
assertEquals(correlation.resolve("k1"), undefined, "the prior job's correlation is released");
|
|
431
432
|
assertEquals(correlation.count(), 1);
|
|
432
433
|
|
|
433
434
|
// The superseded job is not lost — its transcript is flushed and completed, i.e. a replayable past session.
|
|
434
|
-
const priorMeta = service.transcriptOf(
|
|
435
|
+
const priorMeta = service.transcriptOf(composeStreamId("worker-A", "k1"));
|
|
435
436
|
assertEquals(priorMeta?.status, "completed", "the superseded job becomes a completed past session");
|
|
436
|
-
assertEquals(service.reattach(
|
|
437
|
+
assertEquals(service.reattach(composeStreamId("worker-A", "k1"), 0)?.entries.length, 1, "the past session replays its captured terminal");
|
|
437
438
|
service.teardown();
|
|
438
439
|
});
|
|
439
440
|
|
|
@@ -456,15 +457,15 @@ test("H6 durable attribution: completing/superseding a job persists the worker's
|
|
|
456
457
|
|
|
457
458
|
// Job 1 runs, then the worker starts job 2 on the same live connection → job 1 is superseded and
|
|
458
459
|
// released. Its attribution must be durably recorded BEFORE the live correlation forgets it.
|
|
459
|
-
hub.handler?.(produce(
|
|
460
|
-
hub.handler?.(produce(
|
|
460
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
|
|
461
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k2"), 1, "job-2 line"), p.conn);
|
|
461
462
|
|
|
462
463
|
const durable = store.get("k1");
|
|
463
464
|
assert(durable !== undefined, "the superseded job's attribution is persisted");
|
|
464
465
|
assertEquals(durable?.instance, "worker-A");
|
|
465
466
|
assertEquals(durable?.identity, "gpu-box-7");
|
|
466
467
|
assertEquals(durable?.host, "us-east-1a");
|
|
467
|
-
assertEquals(durable?.stream,
|
|
468
|
+
assertEquals(durable?.stream, composeStreamId("worker-A", "k1"));
|
|
468
469
|
assertEquals(durable?.completedAt, "2024-01-02T03:04:05.000Z");
|
|
469
470
|
// The still-active job is NOT yet recorded (attribution is written on completion, not on link).
|
|
470
471
|
assertEquals(store.get("k2"), undefined, "the active job has no completion attribution yet");
|
|
@@ -489,13 +490,13 @@ test("#544 element-instance enrichment: link-time resolution keys the completed
|
|
|
489
490
|
const p = connect("prod", registry);
|
|
490
491
|
|
|
491
492
|
// First produce links the job and fires the (async) element-instance resolution.
|
|
492
|
-
hub.handler?.(produce(
|
|
493
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
|
|
493
494
|
await tick();
|
|
494
495
|
// The live correlation context is enriched while the job runs.
|
|
495
496
|
assertEquals(correlation.resolve("k1")?.elementInstanceKey, "ei-1", "the live context carries the element instance");
|
|
496
497
|
|
|
497
498
|
// Superseding with a new job completes k1 → its attribution persists WITH the element-instance key.
|
|
498
|
-
hub.handler?.(produce(
|
|
499
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k2"), 1, "job-2 line"), p.conn);
|
|
499
500
|
const durable = store.get("k1");
|
|
500
501
|
assertEquals(durable?.elementInstanceKey, "ei-1", "the completed session is keyed on the element instance");
|
|
501
502
|
service.teardown();
|
|
@@ -521,8 +522,8 @@ test("#544 element-instance enrichment: a resolution that lands AFTER completion
|
|
|
521
522
|
|
|
522
523
|
// Link job k1 (fires the still-pending resolution), then supersede it → k1 completes and persists
|
|
523
524
|
// its attribution BEFORE the element instance is known.
|
|
524
|
-
hub.handler?.(produce(
|
|
525
|
-
hub.handler?.(produce(
|
|
525
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
|
|
526
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k2"), 1, "job-2 line"), p.conn);
|
|
526
527
|
assertEquals(store.get("k1")?.elementInstanceKey, undefined, "persisted before the element instance resolved");
|
|
527
528
|
assertEquals(correlation.resolve("k1"), undefined, "k1's live correlation was already released");
|
|
528
529
|
|
|
@@ -544,9 +545,9 @@ test("#544 element-instance enrichment: an unresolved job (never parked) leaves
|
|
|
544
545
|
resolveElementInstance: () => Promise.resolve(undefined),
|
|
545
546
|
});
|
|
546
547
|
const p = connect("prod", registry);
|
|
547
|
-
hub.handler?.(produce(
|
|
548
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
|
|
548
549
|
await tick();
|
|
549
|
-
hub.handler?.(produce(
|
|
550
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k2"), 1, "job-2 line"), p.conn);
|
|
550
551
|
const durable = store.get("k1");
|
|
551
552
|
assert(durable !== undefined, "the session is still attributed");
|
|
552
553
|
assertEquals(durable?.elementInstanceKey, undefined, "no element-instance key when the job was not resolvable");
|
|
@@ -580,14 +581,14 @@ test("#661 primary release: a terminal lifecycle event clears an idle-but-connec
|
|
|
580
581
|
const p = connect("prod", registry);
|
|
581
582
|
|
|
582
583
|
// The worker relays its job's terminal → linked as active.
|
|
583
|
-
hub.handler?.(produce(
|
|
584
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
|
|
584
585
|
assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "the job is active while it runs");
|
|
585
586
|
assertEquals(correlation.count(), 1);
|
|
586
587
|
|
|
587
588
|
// The job ends: the worker emits the terminal `lifecycle` event on the SAME live connection and then
|
|
588
589
|
// goes idle — it does NOT disconnect and does NOT take a new job (no supersede). Before this fix that
|
|
589
590
|
// finished job lingered forever as a phantom active job; now the terminal event releases it.
|
|
590
|
-
hub.handler?.(lifecycle(
|
|
591
|
+
hub.handler?.(lifecycle(composeStreamId("worker-A", "k1"), 1, "completed"), p.conn);
|
|
591
592
|
assertEquals(correlation.jobKeysFor("worker-A"), [], "the finished job is released on the terminal event");
|
|
592
593
|
assertEquals(correlation.count(), 0, "count() drops — no phantom active job");
|
|
593
594
|
|
|
@@ -602,9 +603,9 @@ test("#661 primary release: a terminal lifecycle event clears an idle-but-connec
|
|
|
602
603
|
assert(row.live, "the worker is still connected — the connection persists across jobs");
|
|
603
604
|
|
|
604
605
|
// The terminal event itself is captured in the flushed transcript (release runs AFTER the ring append).
|
|
605
|
-
const meta = service.transcriptOf(
|
|
606
|
+
const meta = service.transcriptOf(composeStreamId("worker-A", "k1"));
|
|
606
607
|
assertEquals(meta?.status, "completed", "the finished job becomes a completed past session");
|
|
607
|
-
assertEquals(service.reattach(
|
|
608
|
+
assertEquals(service.reattach(composeStreamId("worker-A", "k1"), 0)?.entries.length, 2, "the terminal event is part of the transcript");
|
|
608
609
|
service.teardown();
|
|
609
610
|
});
|
|
610
611
|
|
|
@@ -615,14 +616,14 @@ test("#661 primary release: an `exited` lifecycle also releases; a non-terminal
|
|
|
615
616
|
const { service, hub } = mkCorrelatedService(registry, memoryDb(), correlation, byConnection);
|
|
616
617
|
const p = connect("prod", registry);
|
|
617
618
|
|
|
618
|
-
hub.handler?.(produce(
|
|
619
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
|
|
619
620
|
// A `phase: "open"` lifecycle is NOT terminal — a genuinely active job must not be cleared.
|
|
620
|
-
hub.handler?.(lifecycle(
|
|
621
|
+
hub.handler?.(lifecycle(composeStreamId("worker-A", "k1"), 1, "open"), p.conn);
|
|
621
622
|
assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "an open lifecycle keeps the active job");
|
|
622
623
|
assertEquals(correlation.count(), 1);
|
|
623
624
|
|
|
624
625
|
// An `exited` lifecycle (a crash/kill the worker still managed to report) IS terminal → released.
|
|
625
|
-
hub.handler?.(lifecycle(
|
|
626
|
+
hub.handler?.(lifecycle(composeStreamId("worker-A", "k1"), 1, "exited"), p.conn);
|
|
626
627
|
assertEquals(correlation.jobKeysFor("worker-A"), [], "an exited lifecycle releases the job");
|
|
627
628
|
assertEquals(correlation.count(), 0);
|
|
628
629
|
service.teardown();
|
|
@@ -639,25 +640,25 @@ test("#710 close release: a job-end `phase:close` marker flushes the durable tra
|
|
|
639
640
|
// job WITHOUT a supersede (no next job) and WITHOUT a disconnect (the channel persists across jobs),
|
|
640
641
|
// so the ONLY job-end signal is the harness's drained `phase:close` marker.
|
|
641
642
|
const N = 4;
|
|
642
|
-
for (let i = 0; i < N; i++) hub.handler?.(produce(
|
|
643
|
+
for (let i = 0; i < N; i++) hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, `job-1 line ${i}`), p.conn);
|
|
643
644
|
assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "the job is active while it runs");
|
|
644
|
-
assertEquals(service.transcriptOf(
|
|
645
|
+
assertEquals(service.transcriptOf(composeStreamId("worker-A", "k1")), undefined, "not flushed while the job runs");
|
|
645
646
|
|
|
646
647
|
// The job completes: the harness drains its outbound relay buffer and emits `phase:close` on the SAME
|
|
647
648
|
// live connection, then goes idle. Before #710 this was NOT a flush trigger — the durable transcript
|
|
648
649
|
// was snapshotted late (or not at all) by a later supersede/disconnect, truncating the tail. Now the
|
|
649
650
|
// close event flushes it deterministically at job-completion time.
|
|
650
|
-
hub.handler?.(close(
|
|
651
|
+
hub.handler?.(close(composeStreamId("worker-A", "k1"), 1), p.conn);
|
|
651
652
|
|
|
652
653
|
assertEquals(correlation.jobKeysFor("worker-A"), [], "the finished job is released on the close marker");
|
|
653
654
|
assertEquals(correlation.count(), 0, "no phantom active job after close");
|
|
654
655
|
|
|
655
|
-
const meta = service.transcriptOf(
|
|
656
|
+
const meta = service.transcriptOf(composeStreamId("worker-A", "k1"));
|
|
656
657
|
assertEquals(meta?.status, "completed", "the job becomes a completed past session at close time");
|
|
657
658
|
// ALL N produced frames PLUS the close marker itself are in the durable transcript — no truncated
|
|
658
659
|
// tail (the close event rides the ring append before the release, like the #661 terminal path).
|
|
659
660
|
assertEquals(
|
|
660
|
-
service.reattach(
|
|
661
|
+
service.reattach(composeStreamId("worker-A", "k1"), 0)?.entries.length,
|
|
661
662
|
N + 1,
|
|
662
663
|
"every relayed byte (and the close marker) is flushed — the tail is not truncated",
|
|
663
664
|
);
|
|
@@ -708,12 +709,12 @@ test("#661 no-regression: an ordinary (non-lifecycle) chunk never clears a genui
|
|
|
708
709
|
const { service, hub } = mkCorrelatedService(registry, memoryDb(), correlation, byConnection);
|
|
709
710
|
const p = connect("prod", registry);
|
|
710
711
|
|
|
711
|
-
hub.handler?.(produce(
|
|
712
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
|
|
712
713
|
// Ordinary terminal output (raw bytes, not a typed envelope) must NOT be read as a job-end signal.
|
|
713
|
-
for (let i = 0; i < 5; i++) hub.handler?.(produce(
|
|
714
|
+
for (let i = 0; i < 5; i++) hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, `output ${i}`), p.conn);
|
|
714
715
|
assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "a running job stays active across ordinary output");
|
|
715
716
|
assertEquals(correlation.count(), 1);
|
|
716
|
-
assertEquals(service.transcriptOf(
|
|
717
|
+
assertEquals(service.transcriptOf(composeStreamId("worker-A", "k1")), undefined, "the live job is not completed");
|
|
717
718
|
service.teardown();
|
|
718
719
|
});
|
|
719
720
|
|
|
@@ -732,7 +733,7 @@ test("#661 defensive reconcile: an unclean exit (no lifecycle) whose engine job
|
|
|
732
733
|
});
|
|
733
734
|
const p = connect("prod", registry);
|
|
734
735
|
|
|
735
|
-
hub.handler?.(produce(
|
|
736
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
|
|
736
737
|
assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "the job links while it runs");
|
|
737
738
|
|
|
738
739
|
// While the engine still parks the job, the reconcile pass leaves a genuinely active job alone.
|
|
@@ -760,7 +761,7 @@ test("#661 defensive reconcile: a transient engine read failure never falsely re
|
|
|
760
761
|
resolveElementInstance,
|
|
761
762
|
});
|
|
762
763
|
const p = connect("prod", registry);
|
|
763
|
-
hub.handler?.(produce(
|
|
764
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
|
|
764
765
|
|
|
765
766
|
await service.reconcileEngineCorrelations();
|
|
766
767
|
assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "a transient engine failure leaves the job linked");
|
|
@@ -774,7 +775,7 @@ test("#661 defensive reconcile: a no-op when no engine resolver is wired", async
|
|
|
774
775
|
const byConnection = new Map([["prod", "worker-A"]]);
|
|
775
776
|
const { service, hub } = mkCorrelatedService(registry, memoryDb(), correlation, byConnection);
|
|
776
777
|
const p = connect("prod", registry);
|
|
777
|
-
hub.handler?.(produce(
|
|
778
|
+
hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
|
|
778
779
|
|
|
779
780
|
// With no element-instance resolver (engine-less host), the safety net cannot query the engine —
|
|
780
781
|
// it must be an inert no-op, leaving the correlation exactly as the primary path manages it.
|
|
@@ -914,12 +915,12 @@ test("H6 correlation write-side: a throwing correlation.link() never escapes the
|
|
|
914
915
|
// The first `produce` links — but the injected correlation throws. It must be swallowed (advisory),
|
|
915
916
|
// so the frame handler does not throw and the stream is left UNLINKED so a later produce retries.
|
|
916
917
|
failLink(true);
|
|
917
|
-
hub.handler?.(produce(
|
|
918
|
+
hub.handler?.(produce(composeStreamId("worker-L", "kL"), 1, "x"), p.conn);
|
|
918
919
|
assertEquals(inner.count(), 0, "a throwing link is swallowed and records no correlation");
|
|
919
920
|
|
|
920
921
|
// A later produce (link now succeeds) retries the link — proving the stream was left unlinked.
|
|
921
922
|
failLink(false);
|
|
922
|
-
hub.handler?.(produce(
|
|
923
|
+
hub.handler?.(produce(composeStreamId("worker-L", "kL"), 1, "y"), p.conn);
|
|
923
924
|
assertEquals(inner.jobKeysFor("worker-L"), ["kL"], "the link retries and succeeds on a later frame");
|
|
924
925
|
assertEquals(inner.count(), 1);
|
|
925
926
|
service.teardown();
|
|
@@ -940,7 +941,7 @@ test("H6 correlation write-side: a throwing correlation.releaseJob() never escap
|
|
|
940
941
|
instanceForConnection: (id) => byConnection.get(id),
|
|
941
942
|
});
|
|
942
943
|
const p = connect("prod", registry);
|
|
943
|
-
hub.handler?.(produce(
|
|
944
|
+
hub.handler?.(produce(composeStreamId("worker-X", "kX"), 1, "x"), p.conn);
|
|
944
945
|
assertEquals(inner.jobKeysFor("worker-X"), ["kX"]);
|
|
945
946
|
|
|
946
947
|
// A producer disconnect drives #reconcile → #unlink, but releaseJob throws. The advisory contract
|
|
@@ -953,7 +954,7 @@ test("H6 correlation write-side: a throwing correlation.releaseJob() never escap
|
|
|
953
954
|
|
|
954
955
|
// The stream still finalizes (terminal) so #reconcile does not thrash it every frame, and a later
|
|
955
956
|
// frame's reconcile is a clean no-op rather than a repeated crash.
|
|
956
|
-
hub.handler?.(produce(
|
|
957
|
+
hub.handler?.(produce(composeStreamId("worker-X", "kX"), 2, "late"), p.conn);
|
|
957
958
|
assertEquals(
|
|
958
959
|
inner.jobKeysFor("worker-X"),
|
|
959
960
|
["kX"],
|
|
@@ -969,7 +970,7 @@ test("H6 correlation write-side: a producer disconnect releases its job correlat
|
|
|
969
970
|
// No DataLayer → the relay runs unpersisted; correlation release must still fire (store-independent).
|
|
970
971
|
const { service, hub } = mkCorrelatedService(registry, undefined, correlation, byConnection);
|
|
971
972
|
const p = connect("prod", registry);
|
|
972
|
-
hub.handler?.(produce(
|
|
973
|
+
hub.handler?.(produce(composeStreamId("worker-B", "k2"), 1, "x"), p.conn);
|
|
973
974
|
assertEquals(correlation.jobKeysFor("worker-B"), ["k2"]);
|
|
974
975
|
|
|
975
976
|
// Producer drops; a subsequent frame from any live connection reconciles the dead producer.
|
|
@@ -1000,7 +1001,7 @@ test("#689 mid-job reconnect: a stale producer whose instance is still live keep
|
|
|
1000
1001
|
isInstanceLive,
|
|
1001
1002
|
});
|
|
1002
1003
|
const p = connect("conn-old", registry);
|
|
1003
|
-
hub.handler?.(produce(
|
|
1004
|
+
hub.handler?.(produce(composeStreamId("worker-L", "5749"), 1, "booting agent"), p.conn);
|
|
1004
1005
|
assertEquals(correlation.jobKeysFor("worker-L"), ["5749"], "the job is linked on first produce");
|
|
1005
1006
|
|
|
1006
1007
|
// The producer's WS connection blips (client reconnects). Model the reconnect faithfully: the new
|
|
@@ -1014,20 +1015,20 @@ test("#689 mid-job reconnect: a stale producer whose instance is still live keep
|
|
|
1014
1015
|
const other = connect("cons", registry);
|
|
1015
1016
|
hub.handler?.(grant(0), other.conn);
|
|
1016
1017
|
assertEquals(correlation.jobKeysFor("worker-L"), ["5749"], "a mid-job reconnect keeps the correlation");
|
|
1017
|
-
assertEquals(service.transcriptOf(
|
|
1018
|
+
assertEquals(service.transcriptOf(composeStreamId("worker-L", "5749")), undefined, "the still-active stream is NOT archived");
|
|
1018
1019
|
|
|
1019
1020
|
// The worker resumes producing on the SAME job over its already-open NEW connection → re-attributed,
|
|
1020
1021
|
// still one job, transcript still live (not terminal).
|
|
1021
|
-
hub.handler?.(produce(
|
|
1022
|
+
hub.handler?.(produce(composeStreamId("worker-L", "5749"), 1, "resumed output"), p2.conn);
|
|
1022
1023
|
assertEquals(correlation.jobKeysFor("worker-L"), ["5749"], "the resumed producer stays linked to the same job");
|
|
1023
|
-
assertEquals(service.liveFallback(
|
|
1024
|
+
assertEquals(service.liveFallback(composeStreamId("worker-L", "5749"))?.ring !== undefined, true, "the transcript is still live, not completed");
|
|
1024
1025
|
|
|
1025
1026
|
// Only once the worker truly EXITS (all its connections gone → isInstanceLive false) does a later
|
|
1026
1027
|
// reconcile complete the stream and release the correlation.
|
|
1027
1028
|
registry.remove("conn-new");
|
|
1028
1029
|
hub.handler?.(grant(0), other.conn);
|
|
1029
1030
|
assertEquals(correlation.jobKeysFor("worker-L"), [], "a true worker-exit completes + releases the job");
|
|
1030
|
-
assertEquals(service.transcriptOf(
|
|
1031
|
+
assertEquals(service.transcriptOf(composeStreamId("worker-L", "5749"))?.status, "completed", "the exited worker's stream is archived");
|
|
1031
1032
|
service.teardown();
|
|
1032
1033
|
});
|
|
1033
1034
|
|
|
@@ -1039,14 +1040,14 @@ test("#689 mid-job reconnect: with no isInstanceLive wired, a producer disconnec
|
|
|
1039
1040
|
// preserving the prior always-complete-on-disconnect behaviour for callers that don't wire presence.
|
|
1040
1041
|
const { service, hub } = mkCorrelatedService(registry, memoryDb(), correlation, byConnection);
|
|
1041
1042
|
const p = connect("prod", registry);
|
|
1042
|
-
hub.handler?.(produce(
|
|
1043
|
+
hub.handler?.(produce(composeStreamId("worker-N", "kN"), 1, "x"), p.conn);
|
|
1043
1044
|
assertEquals(correlation.jobKeysFor("worker-N"), ["kN"]);
|
|
1044
1045
|
|
|
1045
1046
|
registry.remove("prod");
|
|
1046
1047
|
const other = connect("cons", registry);
|
|
1047
1048
|
hub.handler?.(grant(0), other.conn);
|
|
1048
1049
|
assertEquals(correlation.jobKeysFor("worker-N"), [], "disconnect completes + releases when liveness is unknown");
|
|
1049
|
-
assertEquals(service.transcriptOf(
|
|
1050
|
+
assertEquals(service.transcriptOf(composeStreamId("worker-N", "kN"))?.status, "completed");
|
|
1050
1051
|
service.teardown();
|
|
1051
1052
|
});
|
|
1052
1053
|
|
|
@@ -1065,7 +1066,7 @@ test("#691 engine-owned disconnect: a mid-job reconnect whose engine job is stil
|
|
|
1065
1066
|
resolveElementInstance,
|
|
1066
1067
|
});
|
|
1067
1068
|
const p = connect("conn-old", registry);
|
|
1068
|
-
hub.handler?.(produce(
|
|
1069
|
+
hub.handler?.(produce(composeStreamId("worker-E", "9001"), 1, "booting agent"), p.conn);
|
|
1069
1070
|
assertEquals(correlation.jobKeysFor("worker-E"), ["9001"], "the job links on first produce");
|
|
1070
1071
|
|
|
1071
1072
|
// The producer WS blips: the old connection drops (no new one yet — presence would report the
|
|
@@ -1075,20 +1076,20 @@ test("#691 engine-owned disconnect: a mid-job reconnect whose engine job is stil
|
|
|
1075
1076
|
hub.handler?.(grant(0), other.conn);
|
|
1076
1077
|
await tick(); // let the fire-and-forget engine reconcile settle
|
|
1077
1078
|
assertEquals(correlation.jobKeysFor("worker-E"), ["9001"], "a still-parked job survives the disconnect");
|
|
1078
|
-
assertEquals(service.transcriptOf(
|
|
1079
|
-
assertEquals(service.liveFallback(
|
|
1079
|
+
assertEquals(service.transcriptOf(composeStreamId("worker-E", "9001")), undefined, "the still-active stream is NOT archived");
|
|
1080
|
+
assertEquals(service.liveFallback(composeStreamId("worker-E", "9001"))?.ring !== undefined, true, "the transcript is still live");
|
|
1080
1081
|
|
|
1081
1082
|
// The worker resumes producing on a NEW connection over the same job → re-attributed, still one job.
|
|
1082
1083
|
byConnection.set("conn-new", "worker-E");
|
|
1083
1084
|
const p2 = connect("conn-new", registry);
|
|
1084
|
-
hub.handler?.(produce(
|
|
1085
|
+
hub.handler?.(produce(composeStreamId("worker-E", "9001"), 1, "resumed output"), p2.conn);
|
|
1085
1086
|
assertEquals(correlation.jobKeysFor("worker-E"), ["9001"], "the resumed producer stays linked to the same job");
|
|
1086
1087
|
|
|
1087
1088
|
// The job genuinely ends: the engine park vanishes. The periodic backstop pass completes + archives it.
|
|
1088
1089
|
parked = false;
|
|
1089
1090
|
await service.reconcileEngineCorrelations();
|
|
1090
1091
|
assertEquals(correlation.jobKeysFor("worker-E"), [], "the ended job is released");
|
|
1091
|
-
assertEquals(service.transcriptOf(
|
|
1092
|
+
assertEquals(service.transcriptOf(composeStreamId("worker-E", "9001"))?.status, "completed", "and its transcript is archived");
|
|
1092
1093
|
service.teardown();
|
|
1093
1094
|
});
|
|
1094
1095
|
|
|
@@ -1103,7 +1104,7 @@ test("#691 engine-owned disconnect: a true exit whose engine job is gone complet
|
|
|
1103
1104
|
resolveElementInstance,
|
|
1104
1105
|
});
|
|
1105
1106
|
const p = connect("prod", registry);
|
|
1106
|
-
hub.handler?.(produce(
|
|
1107
|
+
hub.handler?.(produce(composeStreamId("worker-X", "kX"), 1, "x"), p.conn);
|
|
1107
1108
|
assertEquals(correlation.jobKeysFor("worker-X"), ["kX"]);
|
|
1108
1109
|
|
|
1109
1110
|
registry.remove("prod");
|
|
@@ -1111,7 +1112,7 @@ test("#691 engine-owned disconnect: a true exit whose engine job is gone complet
|
|
|
1111
1112
|
hub.handler?.(grant(0), other.conn);
|
|
1112
1113
|
await tick(); // the fire-and-forget engine reconcile resolves "gone" → completes
|
|
1113
1114
|
assertEquals(correlation.jobKeysFor("worker-X"), [], "a truly-ended job is released on disconnect");
|
|
1114
|
-
assertEquals(service.transcriptOf(
|
|
1115
|
+
assertEquals(service.transcriptOf(composeStreamId("worker-X", "kX"))?.status, "completed", "and its transcript is archived");
|
|
1115
1116
|
service.teardown();
|
|
1116
1117
|
});
|
|
1117
1118
|
|
|
@@ -1132,7 +1133,7 @@ test("#691 engine-owned disconnect: an UNLINKED job stream (register/produce rac
|
|
|
1132
1133
|
resolveElementInstance,
|
|
1133
1134
|
});
|
|
1134
1135
|
const p = connect("conn-old", registry);
|
|
1135
|
-
hub.handler?.(produce(
|
|
1136
|
+
hub.handler?.(produce(composeStreamId("worker-R", "7742"), 1, "booting agent"), p.conn);
|
|
1136
1137
|
assertEquals(correlation.count(), 0, "the stream did NOT link — the instance was not resolvable at produce time");
|
|
1137
1138
|
|
|
1138
1139
|
// The old producer connection blips before a later produce could retry the link. A frame drives
|
|
@@ -1142,23 +1143,23 @@ test("#691 engine-owned disconnect: an UNLINKED job stream (register/produce rac
|
|
|
1142
1143
|
hub.handler?.(grant(0), other.conn);
|
|
1143
1144
|
await tick(); // let the fire-and-forget engine reconcile settle
|
|
1144
1145
|
assertEquals(
|
|
1145
|
-
service.transcriptOf(
|
|
1146
|
+
service.transcriptOf(composeStreamId("worker-R", "7742")),
|
|
1146
1147
|
undefined,
|
|
1147
1148
|
"an unlinked-but-still-parked job is NOT archived on disconnect",
|
|
1148
1149
|
);
|
|
1149
|
-
assertEquals(service.liveFallback(
|
|
1150
|
+
assertEquals(service.liveFallback(composeStreamId("worker-R", "7742"))?.ring !== undefined, true, "its transcript is still live");
|
|
1150
1151
|
|
|
1151
1152
|
// The worker resumes on a NEW connection; the instance now resolves → the late link finally lands.
|
|
1152
1153
|
byConnection.set("conn-new", "worker-R");
|
|
1153
1154
|
const p2 = connect("conn-new", registry);
|
|
1154
|
-
hub.handler?.(produce(
|
|
1155
|
+
hub.handler?.(produce(composeStreamId("worker-R", "7742"), 1, "resumed output"), p2.conn);
|
|
1155
1156
|
assertEquals(correlation.jobKeysFor("worker-R"), ["7742"], "the late link succeeds on reconnect once the instance resolves");
|
|
1156
1157
|
|
|
1157
1158
|
// The job genuinely ends: the engine park vanishes → the backstop pass completes + archives it.
|
|
1158
1159
|
parked = false;
|
|
1159
1160
|
await service.reconcileEngineCorrelations();
|
|
1160
1161
|
assertEquals(correlation.jobKeysFor("worker-R"), [], "the ended job is released");
|
|
1161
|
-
assertEquals(service.transcriptOf(
|
|
1162
|
+
assertEquals(service.transcriptOf(composeStreamId("worker-R", "7742"))?.status, "completed", "and its transcript is archived");
|
|
1162
1163
|
service.teardown();
|
|
1163
1164
|
});
|
|
1164
1165
|
|
|
@@ -1180,7 +1181,7 @@ test("#708 periodic backstop covers an UNLINKED job stream: an unlinked-but-park
|
|
|
1180
1181
|
resolveElementInstance,
|
|
1181
1182
|
});
|
|
1182
1183
|
const p = connect("conn-old", registry);
|
|
1183
|
-
hub.handler?.(produce(
|
|
1184
|
+
hub.handler?.(produce(composeStreamId("wk", "7708"), 1, "booting agent"), p.conn);
|
|
1184
1185
|
assertEquals(correlation.count(), 0, "the stream did NOT link — the instance was not resolvable at produce time");
|
|
1185
1186
|
|
|
1186
1187
|
// Producer blips; the disconnect engine-reconcile keeps the still-parked stream live and clears
|
|
@@ -1190,7 +1191,7 @@ test("#708 periodic backstop covers an UNLINKED job stream: an unlinked-but-park
|
|
|
1190
1191
|
hub.handler?.(grant(0), other.conn);
|
|
1191
1192
|
await tick();
|
|
1192
1193
|
assertEquals(
|
|
1193
|
-
service.transcriptOf(
|
|
1194
|
+
service.transcriptOf(composeStreamId("wk", "7708")),
|
|
1194
1195
|
undefined,
|
|
1195
1196
|
"an unlinked-but-still-parked job is NOT archived on disconnect",
|
|
1196
1197
|
);
|
|
@@ -1200,11 +1201,11 @@ test("#708 periodic backstop covers an UNLINKED job stream: an unlinked-but-park
|
|
|
1200
1201
|
parked = false;
|
|
1201
1202
|
await service.reconcileEngineCorrelations();
|
|
1202
1203
|
assertEquals(
|
|
1203
|
-
service.transcriptOf(
|
|
1204
|
+
service.transcriptOf(composeStreamId("wk", "7708"))?.status,
|
|
1204
1205
|
"completed",
|
|
1205
1206
|
"the periodic backstop completes an unlinked job stream once the engine park disappears",
|
|
1206
1207
|
);
|
|
1207
|
-
assertEquals(service.liveFallback(
|
|
1208
|
+
assertEquals(service.liveFallback(composeStreamId("wk", "7708")), undefined, "and its live ring is retired");
|
|
1208
1209
|
service.teardown();
|
|
1209
1210
|
});
|
|
1210
1211
|
|
|
@@ -1224,21 +1225,21 @@ test("#691 engine-owned disconnect: a transient engine read at disconnect never
|
|
|
1224
1225
|
resolveElementInstance,
|
|
1225
1226
|
});
|
|
1226
1227
|
const p = connect("prod", registry);
|
|
1227
|
-
hub.handler?.(produce(
|
|
1228
|
+
hub.handler?.(produce(composeStreamId("worker-T", "kT"), 1, "x"), p.conn);
|
|
1228
1229
|
|
|
1229
1230
|
registry.remove("prod");
|
|
1230
1231
|
const other = connect("cons", registry);
|
|
1231
1232
|
hub.handler?.(grant(0), other.conn);
|
|
1232
1233
|
await tick(); // the engine read rejects → the stream must stay linked, not complete
|
|
1233
1234
|
assertEquals(correlation.jobKeysFor("worker-T"), ["kT"], "a transient engine failure leaves the job linked");
|
|
1234
|
-
assertEquals(service.transcriptOf(
|
|
1235
|
+
assertEquals(service.transcriptOf(composeStreamId("worker-T", "kT")), undefined, "and its transcript is NOT archived");
|
|
1235
1236
|
|
|
1236
1237
|
// The engine recovers and now reports the job gone: the periodic backstop pass completes it.
|
|
1237
1238
|
failing = false;
|
|
1238
1239
|
gone = true;
|
|
1239
1240
|
await service.reconcileEngineCorrelations();
|
|
1240
1241
|
assertEquals(correlation.jobKeysFor("worker-T"), [], "the backstop pass releases the ended job");
|
|
1241
|
-
assertEquals(service.transcriptOf(
|
|
1242
|
+
assertEquals(service.transcriptOf(composeStreamId("worker-T", "kT"))?.status, "completed", "and archives its transcript");
|
|
1242
1243
|
service.teardown();
|
|
1243
1244
|
});
|
|
1244
1245
|
|
|
@@ -1255,10 +1256,10 @@ test("H6 correlation write-side: non-job streams are never linked; a link retrie
|
|
|
1255
1256
|
|
|
1256
1257
|
// A job stream whose producer's presence instance is not yet known does not link — but a later
|
|
1257
1258
|
// frame (after the register lands) retries and links, closing the register/produce race.
|
|
1258
|
-
hub.handler?.(produce(
|
|
1259
|
+
hub.handler?.(produce(composeStreamId("worker-C", "k3"), 1, "a"), p.conn);
|
|
1259
1260
|
assertEquals(correlation.count(), 0, "no presence instance yet → not linked");
|
|
1260
1261
|
byConnection.set("prod", "worker-C");
|
|
1261
|
-
hub.handler?.(produce(
|
|
1262
|
+
hub.handler?.(produce(composeStreamId("worker-C", "k3"), 1, "b"), p.conn);
|
|
1262
1263
|
assertEquals(correlation.jobKeysFor("worker-C"), ["k3"], "retried link succeeds once the instance resolves");
|
|
1263
1264
|
service.teardown();
|
|
1264
1265
|
});
|