@inetafrica/open-claudia 3.4.6 → 3.4.11

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.
@@ -79,6 +79,28 @@ async function testSpeakSerialization() {
79
79
  await session.close();
80
80
  }
81
81
 
82
+ async function testMeasuredMediaReady() {
83
+ const { session } = makeSession();
84
+ let ready = 0, sent = 0;
85
+ session.onMediaReady = () => { ready++; };
86
+ await session._subscribe(901, "peer");
87
+ const codec = new OpusAudioCodec();
88
+ session._onFeedRtp(901, { payload: toneOpus(codec, 30) });
89
+ assert.strictEqual(ready, 0, "receive-only media is insufficient");
90
+ session._writeFrame(Buffer.alloc(WEBRTC_SAMPLES * 2));
91
+ assert.strictEqual(ready, 0, "missing publisher track cannot count as sent media");
92
+ session.publisher = { track: { writeRtp: () => { sent++; } }, sender: { codec: { payloadType: 111 } } };
93
+ session._writeFrame(Buffer.alloc(WEBRTC_SAMPLES * 2));
94
+ assert.strictEqual(ready, 1);
95
+ session._writeFrame(Buffer.alloc(WEBRTC_SAMPLES * 2));
96
+ session._onFeedRtp(901, { payload: toneOpus(codec, 30) });
97
+ assert.strictEqual(ready, 1, "media-ready is reported once per session");
98
+ await session.close();
99
+ session._writeFrame(Buffer.alloc(WEBRTC_SAMPLES * 2));
100
+ assert.strictEqual(sent, 2, "closed sessions cannot send or activate media");
101
+ codec.close();
102
+ }
103
+
82
104
  async function testSpeakCancellation() {
83
105
  let frames = 0;
84
106
  const synthCalls = [];
@@ -421,9 +443,49 @@ async function testCloseResolvesSpeech() {
421
443
  assert.strictEqual(await session.speak("after"), false, "speak after close is a no-op");
422
444
  }
423
445
 
446
+ async function testManagedRoomCredentials() {
447
+ class SignedJanus extends FakeJanus {
448
+ constructor(options) { super(); this.options = options; this.calls = []; }
449
+ async message(handle, body) {
450
+ this.calls.push(body);
451
+ if (body.request === "join" && body.ptype === "publisher") return { plugindata: { data: { videoroom: "joined", private_id: 999, publishers: [] } } };
452
+ return {};
453
+ }
454
+ }
455
+ let refreshes = 0;
456
+ const session = new KazeeCallSession({ roomId: 7, chatId: "chat", callerId: "owner", botUserId: "bot", janusUrl: "ws://test",
457
+ janusToken: "root-token", roomToken: "publisher-token", roomTokenExpiresAt: Date.now() + 300000, managedRoom: true, asCaller: true,
458
+ refreshRoomAccess: async () => { refreshes += 1; return { janusToken: "fresh-root", janusTokenExpiresAt: Date.now() + 86400000, roomToken: "subscriber-token", roomTokenExpiresAt: Date.now() + 300000 }; },
459
+ JanusClientImpl: SignedJanus, PeerConnectionImpl: FakePC, SpeechStreamImpl: FakeSpeechStream });
460
+ session._maybePublish = async () => {};
461
+ await session.start();
462
+ assert.strictEqual(session.janus.options.token, "root-token");
463
+ assert.strictEqual(session.janus.calls[0].token, "publisher-token");
464
+ session.roomTokenExpiresAt = Date.now() - 1;
465
+ await Promise.all([session._subscribe(17, "owner"), session._subscribe(18, "other")]);
466
+ const subJoin = session.janus.calls.find(body => body.ptype === "subscriber");
467
+ assert.strictEqual(subJoin.token, "subscriber-token");
468
+ assert.strictEqual(subJoin.private_id, 999);
469
+ assert.strictEqual(refreshes, 1, "simultaneous subscriber joins share one token refresh");
470
+ session.janusTokenExpiresAt = Date.now() + 50000;
471
+ const handlesBefore = session.janus.handles;
472
+ await session._enforceLimits();
473
+ await wait(0);
474
+ assert.strictEqual(session.janus.token, "fresh-root");
475
+ assert.strictEqual(refreshes, 2, "root renewal does not wait for a new subscriber");
476
+ assert.strictEqual(session.janus.handles, handlesBefore, "renewal keeps existing session and handles");
477
+ session.janusTokenExpiresAt = Date.now() - 1;
478
+ await session._enforceLimits();
479
+ assert.strictEqual(session.closed, true, "expired transport credentials close media");
480
+ await session.close();
481
+ assert.ok(!session.janus.calls.some(body => ["create", "destroy"].includes(body.request)), "server allocated room ownership never transfers to the bot");
482
+ }
483
+
424
484
  (async () => {
485
+ await testManagedRoomCredentials();
425
486
  await testSpeakSerialization();
426
487
  await testSpeakCancellation();
488
+ await testMeasuredMediaReady();
427
489
  await testEchoGateAndBargeIn();
428
490
  await testPrebufferAndCommit();
429
491
  await testHoldForWords();
@@ -6,7 +6,8 @@ const assert = require("assert");
6
6
  const fs = require("fs");
7
7
  const os = require("os");
8
8
  const path = require("path");
9
- const { spawnSync } = require("child_process");
9
+ const { spawn, spawnSync } = require("child_process");
10
+ const { killProcessTree } = require("./core/process-tree");
10
11
 
11
12
  const RESULT_PREFIX = "__PROVIDER_ENFORCER_RESULT__";
12
13
 
@@ -73,7 +74,6 @@ function auditSink() {
73
74
 
74
75
  async function providerProbe(kind) {
75
76
  const root = isolate(kind);
76
- const timeoutSignal = path.join(root, `${kind}-timeout-signal.json`);
77
77
  const controls = [
78
78
  { FAKE_AGENT_TEXT: '{"decision":"allow","reason":"inside mandate"}' },
79
79
  { FAKE_AGENT_TEXT: '{"decision":"block","reason":"outside mandate"}' },
@@ -82,16 +82,27 @@ async function providerProbe(kind) {
82
82
  // The guard-reliability fix retries ONCE on a transient timeout before
83
83
  // failing closed: BOTH attempts hang, so the verdict still escalates —
84
84
  // proving the retry is bounded and never fails open at the transport level.
85
- { FAKE_AGENT_SCENARIO: "forced-termination", FAKE_AGENT_SIGNAL_CAPTURE: timeoutSignal },
86
85
  { FAKE_AGENT_SCENARIO: "forced-termination" },
86
+ { FAKE_AGENT_SCENARIO: "forced-termination", FAKE_AGENT_IGNORE_SIGTERM: "1" },
87
87
  ];
88
88
  const entry = makeProvider(kind, root, controls);
89
89
  const model = `${kind}-enforcer-exact`;
90
90
  const { createUtilityAgentService } = require("./core/utility-agent");
91
+ const children = [];
92
+ const terminations = [];
91
93
  const utility = createUtilityAgentService({
92
94
  registry: registry([entry]),
93
95
  env: { ENFORCER_PROVIDER: kind, [`ENFORCER_MODEL_${kind.toUpperCase()}`]: model },
94
96
  killGraceMs: 300,
97
+ spawnProcess(...args) {
98
+ const child = spawn(...args);
99
+ children.push(child);
100
+ return child;
101
+ },
102
+ killTree(pid, signal) {
103
+ terminations.push({ pid, signal });
104
+ return killProcessTree(pid, signal);
105
+ },
95
106
  });
96
107
  const enforcer = require("./core/enforcer");
97
108
  assert.strictEqual(typeof enforcer.createEnforcerService, "function", "enforcer exposes an injectable utility-provider service");
@@ -114,8 +125,8 @@ async function providerProbe(kind) {
114
125
  spawnUtilityAgent: utility.spawnUtilityAgent,
115
126
  audit,
116
127
  // Bounded (this deadline MUST fire — the child hangs by design), but with
117
- // enough headroom for the hung child to boot on a loaded CI runner (write
118
- // capture #5, arm its SIGTERM recorder) before the deadline kills it.
128
+ // enough headroom for the hung children to boot on a loaded CI runner
129
+ // before the deadline kills them; the retry deliberately ignores TERM.
119
130
  timeoutMs: 5000,
120
131
  });
121
132
  const timedOut = await timeoutService.vetReply(speaker, "timeout verdict fixture");
@@ -154,7 +165,19 @@ async function providerProbe(kind) {
154
165
  assert.ok(record.argv.includes(kind === "claude" ? "plan" : "read-only"));
155
166
  assert.ok(record.argv.includes(kind === "claude" ? "--json-schema" : "--output-schema"));
156
167
  }
157
- assert.strictEqual(JSON.parse(fs.readFileSync(timeoutSignal, "utf8")).signal, "SIGTERM");
168
+ // Observe shutdown from the surviving parent. A child-written receipt can
169
+ // be truncated by SIGKILL during synchronous I/O (CI pipeline 12017), even
170
+ // though the guard correctly failed closed and waited for process close.
171
+ assert.strictEqual(children.length, 6);
172
+ for (const child of children.slice(4)) {
173
+ const signals = terminations.filter((entry) => entry.pid === child.pid).map((entry) => entry.signal);
174
+ assert.strictEqual(signals[0], "SIGTERM", "both timeout attempts first request graceful termination");
175
+ assert.ok(["SIGTERM", "SIGKILL"].includes(child.signalCode), "the timed-out child has exited by signal before the verdict settles");
176
+ assert.throws(() => process.kill(child.pid, 0), { code: "ESRCH" }, "the timed-out provider must not survive its verdict");
177
+ }
178
+ assert.ok(terminations.some((entry) => entry.pid === children[5].pid && entry.signal === "SIGKILL"), "the retry that ignores TERM is forcibly terminated after the grace period");
179
+ assert.strictEqual(children[5].signalCode, "SIGKILL");
180
+ assert.deepStrictEqual(utility.stats(), { activeGlobal: 0, activeByPurpose: {} }, "both timed-out attempts release their concurrency slots after close");
158
181
 
159
182
  return { kind, decisions: [allow.decision, block.decision, escalate.decision, malformed.decision, timedOut.decision] };
160
183
  }
@@ -463,7 +463,8 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
463
463
  // Codex turn. Live probe: unified-exec shell calls still reach PreToolUse as
464
464
  // tool_name "Bash" and the deny gate caught the probe, so the gate no longer
465
465
  // requires unified_exec off. Push approved by Sumeet before release.
466
- assert.strictEqual(pkg.version, "3.4.6", "release version must remain unchanged without explicit approval");
466
+ // 3.4.11: owner approved end-to-end calling rollout on 2026-09-05.
467
+ assert.strictEqual(pkg.version, "3.4.11", "release version must remain unchanged without explicit approval");
467
468
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
468
469
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
469
470
  }
@@ -633,6 +633,88 @@ async function busyRestartAndMissedProbe(createSchedulerService) {
633
633
  assert.strictEqual(missedTimers.length, 1, "a missed wakeup gets exactly one startup attempt");
634
634
  }
635
635
 
636
+ async function durablePendingProbe(createSchedulerService, jobsApi) {
637
+ const configDir = fs.mkdtempSync(path.join(os.tmpdir(), "oc-pending-restart-"));
638
+ let clock = 1_720_000_000_500;
639
+ const openStore = () => jobsApi.createJobsStore({ configDir, jobsFile: path.join(configDir, "jobs.json"), now: () => clock });
640
+ const success = { ok: true, status: "succeeded", delivery: { ok: true } };
641
+ try {
642
+ for (const kind of ["wakeup", "watch", "expiry"]) {
643
+ const store = openStore();
644
+ store.add(baseJob({ id: kind, ...(kind === "wakeup" ? {} : {
645
+ kind: "cron", schedule: "* * * * *", check: "fixture-check", repeat: true,
646
+ expiresAt: kind === "expiry" ? clock - 1 : clock + 1000,
647
+ }) }));
648
+ let checks = 0;
649
+ const h = schedulerHarness(createSchedulerService, [], {
650
+ jobsStore: store, now: () => clock,
651
+ runWatchCheck: async () => { checks++; return { code: kind === "expiry" ? 1 : 0 }; },
652
+ });
653
+ h.state.runningProcess = { pid: 1 };
654
+ for (let tick = 0; tick < 12; tick++) {
655
+ clock += 60 * 60 * 1000;
656
+ const result = await h.service.fireJob(kind);
657
+ assert.strictEqual(result.deferred, true, "hours of busy time must remain queued");
658
+ }
659
+ assert.strictEqual(store.get(kind).attemptCount, 0);
660
+ assert.strictEqual(store.get(kind).disabled, false);
661
+ assert.strictEqual(checks, kind === "wakeup" ? 0 : 1);
662
+ assert.strictEqual(h.sends.length, 0);
663
+ h.service.stopAll();
664
+
665
+ // Reopen the real on-disk store and construct a new scheduler, as boot does.
666
+ const reloaded = openStore();
667
+ const terminal = deferred();
668
+ const restarted = schedulerHarness(createSchedulerService, [], {
669
+ jobsStore: reloaded, now: () => clock, outcomes: [terminal.promise, success],
670
+ runWatchCheck: async () => { throw new Error("pending watch must not poll after restart"); },
671
+ });
672
+ restarted.service.init();
673
+ const firing = restarted.scheduled[0].fn();
674
+ await nextTurn();
675
+ clock += 60 * 60 * 1000;
676
+ assert.strictEqual((await restarted.service.fireJob(kind)).inFlight, true, "long runs cannot re-enter after retry delay");
677
+ assert.strictEqual(restarted.runCalls.length, 1);
678
+ assert.strictEqual(restarted.sends.length, 1);
679
+ if (kind === "expiry") assert.match(restarted.runCalls[0].prompt, /Watch expired/);
680
+ terminal.resolve(success);
681
+ await firing;
682
+ if (kind === "watch") {
683
+ assert.strictEqual(reloaded.get(kind).status, "pending");
684
+ assert.strictEqual(reloaded.get(kind).watchTriggeredOccurrenceId, null);
685
+ restarted.service.removeJob(kind);
686
+ } else assert.strictEqual(reloaded.get(kind), null);
687
+ restarted.service.stopAll();
688
+ const finalBoot = schedulerHarness(createSchedulerService, [], { jobsStore: openStore(), now: () => clock });
689
+ finalBoot.service.init();
690
+ assert.strictEqual(finalBoot.scheduled.length, 0, "successful one-shot cannot replay on restart");
691
+ }
692
+
693
+ const store = openStore();
694
+ store.add(baseJob({ id: "failure", kind: "cron", schedule: "* * * * *", check: "fixture" }));
695
+ const failed = schedulerHarness(createSchedulerService, [], {
696
+ jobsStore: store, now: () => clock,
697
+ runWatchCheck: async () => ({ code: 0 }), outcomes: [{ ok: false, error: { code: "FIXTURE_FAILED" } }],
698
+ });
699
+ await failed.service.fireJob("failure");
700
+ failed.service.stopAll();
701
+ clock += 2000;
702
+ const retry = schedulerHarness(createSchedulerService, [], {
703
+ jobsStore: openStore(), now: () => clock,
704
+ runWatchCheck: async () => { throw new Error("do not recheck"); },
705
+ outcomes: [{ ok: false }, { ok: false }],
706
+ });
707
+ await retry.service.fireJob("failure");
708
+ await retry.service.fireJob("failure");
709
+ assert.strictEqual(retry.jobsStore.get("failure").status, "dead-letter", "real failures retain bounded retries");
710
+ assert.strictEqual(retry.jobsStore.get("failure").attemptCount, 3);
711
+ assert.strictEqual(retry.sends.length, 0, "advisory is not repeated across failure/restart");
712
+ retry.service.stopAll();
713
+ } finally {
714
+ fs.rmSync(configDir, { recursive: true, force: true });
715
+ }
716
+ }
717
+
636
718
  async function unavailableAndFallbackProbe(createSchedulerService) {
637
719
  const noFallback = schedulerHarness(createSchedulerService, [baseJob({ id: "unavailable" })], {
638
720
  providerStatus: () => ({ availability: { state: "missing" }, auth: { state: "not_checked" } }),
@@ -960,6 +1042,7 @@ async function main() {
960
1042
  await failureAndRetryProbe(schedulerApi.createSchedulerService);
961
1043
  await detachedHistoricalBranchProbe(schedulerApi.createSchedulerService);
962
1044
  await busyRestartAndMissedProbe(schedulerApi.createSchedulerService);
1045
+ await durablePendingProbe(schedulerApi.createSchedulerService, jobsApi);
963
1046
  await unavailableAndFallbackProbe(schedulerApi.createSchedulerService);
964
1047
  await cronOccurrenceProbe(schedulerApi.createSchedulerService);
965
1048
  await watchGateProbe(schedulerApi.createSchedulerService);