@jskit-ai/assistant-core 0.1.213 → 0.1.215

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.
Files changed (59) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/fixtures/responsive-assistant/ConversationFixture.vue +7 -1
  3. package/package.json +5 -5
  4. package/src/client/conversation/AssistantConversationElement.vue +5 -1
  5. package/src/client/conversation/AssistantTranscript.vue +10 -1
  6. package/src/server/conversation/agentRun.js +38 -11
  7. package/src/server/conversation/claudeHistory.js +40 -5
  8. package/src/server/conversation/claudeProcess.js +16 -2
  9. package/src/server/conversation/claudeTurn.js +75 -16
  10. package/src/server/conversation/codexConfiguration.js +1 -1
  11. package/src/server/conversation/codexHistoryAdapter.js +11 -4
  12. package/src/server/conversation/codexNotificationQueue.js +11 -2
  13. package/src/server/conversation/codexObservationControl.js +8 -8
  14. package/src/server/conversation/codexOutput.js +20 -15
  15. package/src/server/conversation/codexProvider.js +40 -3
  16. package/src/server/conversation/codexProviderOwner.js +110 -1
  17. package/src/server/conversation/codexTurn.js +3 -0
  18. package/src/server/conversation/continuity.js +24 -6
  19. package/src/server/conversation/index.js +1 -1
  20. package/src/server/conversation/nativeHistoryExport.js +97 -0
  21. package/src/server/conversation/openCodeClient.js +47 -1
  22. package/src/server/conversation/openCodeRuntime.js +132 -24
  23. package/src/server/conversation/openCodeTurn.js +65 -24
  24. package/src/server/conversation/providers/api.js +7 -0
  25. package/src/server/conversation/providers/claudeDriver.js +52 -7
  26. package/src/server/conversation/providers/codex.js +19 -2
  27. package/src/server/conversation/providers/codexDriver.js +92 -14
  28. package/src/server/conversation/providers/opencodeDriver.js +31 -9
  29. package/src/server/conversation/runtime.js +418 -64
  30. package/src/server/conversation/runtimeStateUpgrade.js +77 -2
  31. package/src/server/conversation/streams.js +25 -5
  32. package/src/server/conversation/tools.js +2 -2
  33. package/src/server/conversation/transcript.js +23 -8
  34. package/src/server/hosts/claudeHistory.js +1 -0
  35. package/src/server/hosts/claudeProcess.js +1 -0
  36. package/src/server/hosts/codexProvider.js +3 -0
  37. package/src/server/hosts/nativeHistory.js +1 -0
  38. package/src/server/hosts/openCodeClient.js +1 -0
  39. package/src/server/hosts/openCodeTurn.js +1 -0
  40. package/src/server/index.js +1 -1
  41. package/src/server/lib/assistantToolLoop.js +99 -2
  42. package/test/aiConnectionClient.test.js +57 -0
  43. package/test/assistantBoundedToolLoop.test.js +332 -0
  44. package/test/assistantScroll.browser.test.js +151 -0
  45. package/test/claudeConversationSession.test.js +1329 -3
  46. package/test/claudeProcess.test.js +101 -0
  47. package/test/claudeTurn.test.js +236 -1
  48. package/test/codexConversationSession.test.js +1257 -12
  49. package/test/codexHistoryAdapter.test.js +74 -1
  50. package/test/codexTurn.test.js +282 -1
  51. package/test/conversation.test.js +83 -0
  52. package/test/conversationAgentRun.test.js +181 -0
  53. package/test/conversationRuntime.test.js +95 -0
  54. package/test/conversationRuntimeStateUpgrade.test.js +89 -0
  55. package/test/conversationStreaming.test.js +85 -0
  56. package/test/conversationTools.test.js +112 -1
  57. package/test/openCodeClient.test.js +186 -0
  58. package/test/openCodeRuntime.test.js +78 -0
  59. package/test/opencodeConversationSession.test.js +637 -5
@@ -276,3 +276,104 @@ test("Claude handshake failure retains acquired execution custody when cleanup i
276
276
  });
277
277
  }
278
278
  });
279
+
280
+
281
+ test("Claude initialization rejects confirmed execution exit before half-open protocol EOF", async () => {
282
+ const { PassThrough, Writable } = await import("node:stream");
283
+ for (const cleanup of ["verified", "unconfirmed", "rejected"]) {
284
+ const requested = Promise.withResolvers();
285
+ const exited = Promise.withResolvers();
286
+ const stdout = new PassThrough();
287
+ const stdin = new Writable({ write(chunk, _encoding, done) {
288
+ requested.resolve(JSON.parse(chunk.toString()));
289
+ done();
290
+ } });
291
+ const cleanupError = new Error("Owned cleanup refused");
292
+ const stops = [];
293
+ const execution = {
294
+ async start() { return { id: "half-open-startup", stdin, stdout, exited: exited.promise }; },
295
+ async stop(id) {
296
+ stops.push(id);
297
+ if (cleanup === "rejected") throw cleanupError;
298
+ return { scopeEmpty: cleanup === "verified" };
299
+ }
300
+ };
301
+ const pending = createClaudeCodeProcess({ execution });
302
+ let failure;
303
+ void pending.catch(error => { failure = error; });
304
+ try {
305
+ assert.equal((await requested.promise).request.subtype, "initialize");
306
+ assert.equal(stdout.readableEnded, false);
307
+ exited.resolve({ code: 1, signal: null });
308
+ // The supplied process receipt must settle startup without a protocol EOF
309
+ // or advancement of the original 30-second control deadline.
310
+ for (let index = 0; index < 5; index += 1) await new Promise(resolve => setImmediate(resolve));
311
+ assert.ok(failure, "Confirmed native exit settles initialization promptly");
312
+ assert.equal(failure.code, "assistant_claude_startup_exited");
313
+ assert.deepEqual(failure.processExit, { code: 1, signal: null });
314
+ assert.equal(failure.executionId, "half-open-startup");
315
+ assert.deepEqual(stops, ["half-open-startup"]);
316
+ assert.equal(failure.stopProof.scopeEmpty, cleanup === "verified");
317
+ assert.equal(failure.cleanupFailed === true, cleanup !== "verified");
318
+ if (cleanup === "rejected") assert.equal(failure.cleanupError, cleanupError);
319
+ } finally {
320
+ stdout.destroy();
321
+ stdin.destroy();
322
+ await pending.catch(() => {});
323
+ }
324
+ }
325
+ });
326
+
327
+ test("Claude initialization preserves rejected execution observation and exact cleanup custody", async () => {
328
+ const { PassThrough, Writable } = await import("node:stream");
329
+ const requested = Promise.withResolvers();
330
+ const exited = Promise.withResolvers();
331
+ const stdout = new PassThrough();
332
+ const stdin = new Writable({ write(_chunk, _encoding, done) { requested.resolve(); done(); } });
333
+ const failure = new Error("Execution observation failed");
334
+ const stops = [];
335
+ const pending = createClaudeCodeProcess({ execution: {
336
+ async start() { return { id: "observed-startup", stdin, stdout, exited: exited.promise }; },
337
+ async stop(id) { stops.push(id); return { scopeEmpty: true }; }
338
+ } });
339
+ const rejected = assert.rejects(pending, error => {
340
+ assert.equal(error, failure);
341
+ assert.equal(error.executionId, "observed-startup");
342
+ assert.deepEqual(error.stopProof, { scopeEmpty: true, exited: true });
343
+ assert.equal(error.cleanupFailed, undefined);
344
+ return true;
345
+ });
346
+ try {
347
+ await requested.promise;
348
+ exited.reject(failure);
349
+ await rejected;
350
+ assert.deepEqual(stops, ["observed-startup"]);
351
+ } finally {
352
+ stdout.destroy();
353
+ stdin.destroy();
354
+ }
355
+ });
356
+
357
+
358
+ test("Claude supplied execution hosts retain initialization without an optional exit receipt", { skip: process.platform === "win32" }, async t => {
359
+ const setup = await fixture(t, `
360
+ import { createInterface } from "node:readline";
361
+ createInterface({ input: process.stdin }).on("line", line => {
362
+ const frame = JSON.parse(line);
363
+ process.stdout.write(JSON.stringify({ type: "control_response", response: {
364
+ subtype: "success", request_id: frame.request_id, response: { originalHost: true }
365
+ } }) + "\\n");
366
+ });
367
+ `);
368
+ const local = createLocalConversationExecution();
369
+ t.after(() => local.close());
370
+ const native = await createClaudeCodeProcess({ ...setup, execution: {
371
+ async start(input) {
372
+ const { id, stdin, stdout } = await local.start(input);
373
+ return { id, stdin, stdout };
374
+ },
375
+ stop: id => local.stop(id)
376
+ } });
377
+ assert.equal(native.initialization.originalHost, true);
378
+ assert.deepEqual(await native.stop(), { scopeEmpty: true, exited: true });
379
+ });
@@ -275,6 +275,9 @@ import { createClaudeConversationOwner } from "../src/server/conversation/claude
275
275
  import { mkdtemp, rm } from "node:fs/promises";
276
276
  import os from "node:os";
277
277
  import path from "node:path";
278
+ import { createConversationRuntime, createMemoryConversationStorage, createConversationTranscript } from "../src/server/conversation/index.js";
279
+ import { createActionCatalogue } from "@jskit-ai/kernel/server/actions";
280
+ import { createSchema } from "json-rest-schema";
278
281
 
279
282
  // Exercise the original supplied-store Send/process/event owner, without the
280
283
  // standalone execution-release capability or application-tool branch.
@@ -292,9 +295,12 @@ async function publishedClaudeFixture(t, { publication = true, providerId = "ant
292
295
  select() {}, read: (_context, id) => saved.get(id),
293
296
  async save(entry) { saved.set(entry.id, JSON.stringify({ turnId: entry.turn?.id,
294
297
  state: entry.turn?.state, accountIdentity: entry.accountIdentity })); },
295
- async hasMessage() { return false; }
298
+ async hasMessage() { return false; },
299
+ readSessionConversations: () => ({ ids: [...saved.keys()], context })
296
300
  },
297
301
  preparation: {
302
+ context: async () => context,
303
+ entry: () => ({ options: { mainId: conversationId }, check() {} }),
298
304
  check() {}, account: () => ({ identity: behavior.account, providerId }),
299
305
  async configuration() { return { systemPrompt: "Retained host instructions", settings: {}, model: providerId === "deepseek" ? "deepseek-flash" : "sonnet" }; },
300
306
  async message(_entry, _input, { message }) { return message; }
@@ -321,10 +327,12 @@ async function publishedClaudeFixture(t, { publication = true, providerId = "ant
321
327
  if (event.type === "before-state") await beforeState?.(entry, event);
322
328
  if (event.type === "admit-message" && canonicalAdmission) {
323
329
  await behavior.beforeAdmit?.();
330
+ if (behavior.admit) return behavior.admit(event);
324
331
  return { turnId: `authored-${event.messageId}`, messages: [{ role: "user", messageId: event.messageId, text: event.message }] };
325
332
  }
326
333
  if (event.type === "message" && event.message.complete && event.message.role === "assistant") {
327
334
  await behavior.beforePublish?.();
335
+ if (behavior.publish) return behavior.publish(event.message);
328
336
  const turn = { id: `host-${messages.length}`, messages: [{ messageId: event.message.id,
329
337
  outputId: event.message.outputId || event.message.id, role: "assistant", text: event.message.text }] };
330
338
  messages.push(turn);
@@ -704,3 +712,230 @@ test("Claude admitted old executor failure cannot stop its accepted steering suc
704
712
  assert.equal(stops, 0);
705
713
  assert.equal(f.entry.process, native);
706
714
  });
715
+
716
+
717
+ test("Claude verified native cleanup retains interrupted checkpoint refusal without leaving saved work active", async t => {
718
+ const f = await publishedClaudeFixture(t);
719
+ await f.send("checkpoint-refused-stop");
720
+ const native = f.entry.process;
721
+ const refusal = new Error("Saved lesson checkpoint refused");
722
+ let stopped = false;
723
+ native.stop = async () => { stopped = true; return { exited: true, scopeEmpty: true }; };
724
+ f.setBeforeState((_entry, event) => {
725
+ if (event.state === "interrupted") {
726
+ assert.equal(stopped, true, "Original native cleanup precedes the host checkpoint");
727
+ throw refusal;
728
+ }
729
+ });
730
+ const completion = f.entry.completion.promise;
731
+ await assert.rejects(f.entry.nativeTurn.stopProcess("Broken stream"), error => error === refusal);
732
+ assert.equal(f.entry.process, null);
733
+ assert.equal(f.entry.executionId, "");
734
+ assert.deepEqual(f.entry.exitProof, { exited: true, scopeEmpty: true });
735
+ assert.equal(f.entry.turn.active, false);
736
+ assert.equal(f.entry.turn.state, "interrupted");
737
+ assert.equal(f.entry.turn.error, refusal.message);
738
+ const settled = await completion;
739
+ assert.equal(settled.status, "interrupted");
740
+ assert.equal(settled.error, refusal.message);
741
+ assert.equal(f.final(), null, "Refused checkpoint supplies no completed answer receipt");
742
+ await f.reopen();
743
+ assert.equal(f.entry.turn.active, false, "Original supplied store retained the truthful interruption");
744
+ assert.equal(f.entry.turn.state, "interrupted");
745
+ });
746
+
747
+ // Retain the original supplied-store/native process fixture and drive it through
748
+ // the actual common runtime, including its completion-only admitted receipt.
749
+ async function suppliedCommonFixture(t, { tools = true } = {}) {
750
+ const f = await publishedClaudeFixture(t, { canonicalAdmission: true });
751
+ const storage = createMemoryConversationStorage();
752
+ const transcript = createConversationTranscript({ storage });
753
+ const context = { actor: { id: "owner" }, surface: "app" };
754
+ const admissions = [];
755
+ const checkpoints = [];
756
+ const actions = createActionCatalogue();
757
+ actions.register({ contributorId: "lesson", domain: "lesson", actions: [{
758
+ id: "lesson.inspect", version: 1, kind: "query", channels: ["automation"], surfaces: ["app"],
759
+ permission: { require: "authenticated" }, idempotency: "none",
760
+ input: { schema: createSchema({}), mode: "replace" },
761
+ output: { schema: createSchema({}), mode: "replace" }, execute: async () => ({})
762
+ }] });
763
+ let delivery;
764
+ f.behavior.admit = event => transcript.writeConversationUserMessage("lesson", {
765
+ messageId: event.messageId, text: event.message
766
+ });
767
+ f.behavior.publish = message => storage.write("lesson", async transaction => {
768
+ const turnId = await transaction.nextTurnId();
769
+ await transaction.appendMessage(turnId, { ...message, role: "assistant", messageId: message.id });
770
+ const turn = await transaction.readTurn(turnId);
771
+ f.messages.push(turn);
772
+ return turn;
773
+ });
774
+ const binding = { sessionId: "lesson", namespace: f.context.key, engine: "claude", runtime: { store: {} },
775
+ native: { owner: f.owner, preparation: { cleanup: () => ({ context: f.context, application: {
776
+ readContext: async () => f.context, terminals: { close: async () => ({ ok: true }) }
777
+ } }) } },
778
+ admission() {}, prepareInput: async input => input,
779
+ read: async () => ({ threadId: f.conversationId, configuration: { model: "native" },
780
+ turn: f.owner.snapshot(f.entry), delivery }),
781
+ readStream: async () => ({ messages: [] }),
782
+ state: { read: async () => delivery, write: async value => { delivery = structuredClone(value); } },
783
+ transcript: { history: async () => [], readConversationLog: () => transcript.readConversationLog("lesson"),
784
+ hasMessage: id => transcript.conversationMessageIdExists("lesson", id),
785
+ writeUserMessage: input => transcript.writeConversationUserMessage("lesson", input) },
786
+ ...(tools ? { applicationTools: { storage, prepareContext(actual, admission) {
787
+ admissions.push(admission); return actual;
788
+ } } } : {}) };
789
+ const runtime = createConversationRuntime({ ...(tools ? { actions } : {}), authorize: () => true,
790
+ host: { conversation: () => binding } });
791
+ t.after(() => runtime.close());
792
+ const conversation = await runtime.open({ id: "lesson", context });
793
+ f.setBeforeState((entry, event) => {
794
+ if (event.state !== "completed") return;
795
+ assert.equal(entry.turn.active, true, "Retained checkpoint still precedes saved idle state");
796
+ assert.equal(entry.nativeTurn.read().active, false);
797
+ const final = f.final();
798
+ assert.ok(final, "The original canonical FINAL is available at the checkpoint");
799
+ const admission = admissions.at(-1);
800
+ if (tools) admission.assertCompletionCurrent();
801
+ checkpoints.push({ final, admission });
802
+ });
803
+ async function loadContract(id = "contract") {
804
+ await f.frame({ type: "assistant", uuid: `tool-${id}`, message: { content: [
805
+ { type: "tool_use", id, name: "mcp__application__assistant_action_contract",
806
+ input: { actionId: "lesson.inspect", version: 1 } }
807
+ ] } });
808
+ return f.entry.process.options.onControlRequest({ subtype: "mcp_message", server_name: "application",
809
+ message: { jsonrpc: "2.0", id, method: "tools/call", params: { name: "assistant_action_contract",
810
+ arguments: { actionId: "lesson.inspect", version: 1 }, _meta: { "claudecode/toolUseId": id } } }
811
+ }, { signal: new AbortController().signal });
812
+ }
813
+ return { ...f, get entry() { return f.entry; }, runtime, conversation, binding, admissions, checkpoints, loadContract, testSignal: t.signal };
814
+ }
815
+
816
+ async function sendCommon(f, messageId) {
817
+ const receipt = await f.conversation.send({ messageId, text: "Prepare the question." });
818
+ while (!f.entry.command?.admitted) {
819
+ f.testSignal.throwIfAborted();
820
+ await new Promise(resolve => setImmediate(resolve));
821
+ }
822
+ return receipt;
823
+ }
824
+
825
+ for (const tools of [true, false]) test(`Claude supplied common runtime checkpoints after its completion fence with tools=${tools}`, { timeout: 10_000 }, async t => {
826
+ const f = await suppliedCommonFixture(t, { tools });
827
+ const receipt = await sendCommon(f, "common-question");
828
+ assert.equal(receipt.status, "accepted");
829
+ assert.equal(typeof f.entry.command.finishIfCurrent, "function");
830
+ if (tools) {
831
+ await f.loadContract();
832
+ assert.throws(() => f.admissions.at(-1).assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
833
+ } else assert.equal(f.entry.command.tools, undefined);
834
+ await f.frame({ type: "result", subtype: "success", result: "Where would you try the application?", uuid: "question-final" });
835
+ await f.conversation.wait();
836
+ assert.equal(f.checkpoints.length, 1);
837
+ assert.notEqual(f.checkpoints[0].final.conversationTurn.turnId, receipt.turnId,
838
+ "The true native FINAL keeps its separate original carrier");
839
+ assert.equal(f.entry.turn.state, "completed");
840
+ assert.equal(f.entry.command, null);
841
+ if (tools) f.admissions.at(-1).assertCompletionCurrent();
842
+ });
843
+
844
+ test("Claude supplied common completion joins accepted steering and checkpoints only its own final", { timeout: 10_000 }, async t => {
845
+ const f = await suppliedCommonFixture(t);
846
+ await sendCommon(f, "common-first");
847
+ await f.loadContract("first-contract");
848
+ const first = f.admissions.at(-1);
849
+ const legacyCompletion = f.entry.completion.promise;
850
+ const receipt = await f.conversation.send({ messageId: "common-successor", text: "Use this new question.", steer: true });
851
+ assert.equal(receipt.status, "accepted");
852
+ assert.equal(f.entry.completion.promise, legacyCompletion, "Original same-turn steering retains the direct wait owner");
853
+ while (f.entry.command.messageId !== "common-successor") {
854
+ f.testSignal.throwIfAborted();
855
+ await new Promise(resolve => setImmediate(resolve));
856
+ }
857
+ await f.loadContract("successor-contract");
858
+ assert.throws(() => first.assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
859
+ await f.frame({ type: "result", subtype: "success", result: "The successor question", uuid: "successor-final" });
860
+ await f.conversation.wait();
861
+ assert.equal((await legacyCompletion).status, "completed");
862
+ assert.equal(f.checkpoints.length, 1);
863
+ assert.equal(f.checkpoints[0].final.inputMessageId, claudeNativeMessageId("common-successor"));
864
+ assert.equal(f.checkpoints[0].admission.messageId, "common-successor");
865
+ assert.throws(() => first.assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
866
+ });
867
+
868
+ test("Claude supplied completion retains the original waiter when a reserved successor is rejected before dispatch", { timeout: 10_000 }, async t => {
869
+ const f = await suppliedCommonFixture(t);
870
+ await sendCommon(f, "unreplaced-first");
871
+ await f.loadContract();
872
+ const first = f.admissions.at(-1);
873
+ const legacyCompletion = f.entry.completion.promise;
874
+ const preparing = Promise.withResolvers();
875
+ const held = Promise.withResolvers();
876
+ f.binding.prepareInput = async () => { preparing.resolve(); await held.promise; };
877
+ const steering = f.conversation.send({ messageId: "never-dispatched-successor", text: "Rejected before native work.", steer: true });
878
+ void steering.catch(() => {});
879
+ await preparing.promise;
880
+ await f.frame({ type: "result", subtype: "success", result: "The still-current question", uuid: "unreplaced-final" });
881
+ assert.equal(f.checkpoints.length, 0);
882
+ held.reject(new Error("The host rejected preparation before dispatch."));
883
+ await assert.rejects(steering, /host rejected preparation/);
884
+ await f.conversation.wait();
885
+ assert.equal((await legacyCompletion).status, "completed");
886
+ assert.equal(f.checkpoints.length, 1);
887
+ assert.equal(f.checkpoints[0].final.inputMessageId, claudeNativeMessageId("unreplaced-first"));
888
+ first.assertCompletionCurrent();
889
+ });
890
+
891
+ for (const cancelled of [false, true]) test(`Claude supplied native settlement releases its queue while reserved steering waits, cancelled=${cancelled}`, { timeout: 10_000 }, async t => {
892
+ const f = await suppliedCommonFixture(t);
893
+ await sendCommon(f, "reserved-first");
894
+ await f.loadContract();
895
+ const first = f.admissions.at(-1);
896
+ const preparing = Promise.withResolvers();
897
+ const held = Promise.withResolvers();
898
+ f.binding.prepareInput = async input => { preparing.resolve(); await held.promise; return input; };
899
+ const steering = f.conversation.send({ messageId: "reserved-successor", text: "A reserved new instruction.", steer: true });
900
+ void steering.catch(() => {});
901
+ await preparing.promise;
902
+ const legacyCompletion = f.entry.completion.promise;
903
+ let cancellation;
904
+ try {
905
+ await f.frame({ type: "result", subtype: "success", result: "The prior question", uuid: "reserved-final" });
906
+ assert.equal(f.entry.nativeTurn.read().active, false);
907
+ assert.equal(f.entry.turn.active, true);
908
+ assert.equal(f.checkpoints.length, 0, "Native queue does not await the reserved common admission");
909
+ assert.throws(() => first.assertCompletionCurrent(), { code: "conversation_tool_request_retired" });
910
+ if (cancelled) cancellation = f.conversation.cancel();
911
+ } finally { held.resolve(); }
912
+ await cancellation;
913
+ await assert.rejects(steering);
914
+ assert.equal((await legacyCompletion).status, "interrupted", "Legacy wait keeps its original settlement and cleanup result");
915
+ await f.conversation.wait();
916
+ assert.equal(f.checkpoints.length, 0);
917
+ assert.equal(f.final(), null, "Rejected steering or Stop cannot revive the prior FINAL");
918
+ assert.equal(f.entry.process, null);
919
+ assert.equal(f.entry.command, null);
920
+ assert.throws(() => first.assertCompletionCurrent());
921
+ });
922
+
923
+ test("Claude supplied common checkpoint failure preserves original refusal and native cleanup", { timeout: 10_000 }, async t => {
924
+ const f = await suppliedCommonFixture(t);
925
+ await sendCommon(f, "refused-common-question");
926
+ await f.loadContract();
927
+ const refusal = new Error("The original lesson checkpoint refused this completion.");
928
+ f.setBeforeState((entry, event) => {
929
+ if (event.state !== "completed") return;
930
+ assert.equal(entry.turn.active, true);
931
+ f.admissions.at(-1).assertCompletionCurrent();
932
+ throw refusal;
933
+ });
934
+ await f.frame({ type: "result", subtype: "success", result: "Never delivered", uuid: "refused-final" });
935
+ const state = await f.conversation.wait();
936
+ assert.equal(state.error, refusal.message);
937
+ assert.equal(f.final(), null);
938
+ assert.equal(f.entry.process, null);
939
+ assert.equal(f.entry.command, null);
940
+ assert.equal(f.entry.turn.active, false);
941
+ });