kimaki 0.28.0 → 0.30.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/dist/agent-model.e2e.test.js +33 -33
- package/dist/anthropic-account-identity.js +1 -1
- package/dist/anthropic-auth-plugin.js +5 -5
- package/dist/anthropic-auth-plugin.test.js +30 -2
- package/dist/anthropic-auth-state.js +1 -1
- package/dist/bash-tool-schema-plugin.js +18 -0
- package/dist/bash-tool-schema-plugin.test.js +20 -1
- package/dist/bin.js +7 -3
- package/dist/bot-lifecycle.test.js +206 -0
- package/dist/cache-drift-plugin.js +25 -18
- package/dist/cache-rewrite.js +39 -0
- package/dist/cache-rewrite.test.js +41 -0
- package/dist/channel-management.js +113 -51
- package/dist/cli-commands/misc.js +4 -1
- package/dist/cli-commands/project.js +7 -4
- package/dist/cli-commands/session.js +118 -36
- package/dist/cli-commands/thread.js +63 -0
- package/dist/cli-parsing.test.js +3 -3
- package/dist/cli-runner.js +11 -1
- package/dist/cli-send-thread.e2e.test.js +2 -2
- package/dist/cli.js +6 -4
- package/dist/commands/ask-question.js +72 -23
- package/dist/commands/queue.js +84 -36
- package/dist/commands/queue.test.js +15 -0
- package/dist/commands/restart-opencode-server.js +7 -1
- package/dist/commands/session-id.js +5 -5
- package/dist/database.js +149 -6
- package/dist/db.js +34 -0
- package/dist/db.test.js +199 -1
- package/dist/default-channel-provisioning.test.js +2 -1
- package/dist/discord-bot.js +220 -146
- package/dist/discord-command-registration.js +1 -1
- package/dist/external-directory-permissions.e2e.test.js +3 -3
- package/dist/external-directory-project-deny.e2e.test.js +1 -1
- package/dist/forum-sync/watchers.js +15 -0
- package/dist/gateway-proxy.e2e.test.js +9 -9
- package/dist/guild-categories.test.js +225 -0
- package/dist/hrana-server.js +68 -14
- package/dist/interaction-handler.js +5 -1
- package/dist/kimaki-opencode-plugin.js +2 -0
- package/dist/kimaki-opencode-plugin.test.js +28 -0
- package/dist/list-channel-threads.js +163 -0
- package/dist/list-channel-threads.test.js +80 -0
- package/dist/markdown.js +164 -102
- package/dist/markdown.test.js +88 -20
- package/dist/message-formatting.js +29 -18
- package/dist/message-formatting.test.js +111 -3
- package/dist/opencode.js +27 -12
- package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
- package/dist/question-abort-resume.e2e.test.js +100 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +1 -1
- package/dist/question-text-before-dropdown.e2e.test.js +1 -1
- package/dist/queue-advanced-abort.e2e.test.js +3 -3
- package/dist/queue-advanced-action-buttons.e2e.test.js +5 -5
- package/dist/queue-advanced-e2e-setup.js +63 -0
- package/dist/queue-advanced-footer.e2e.test.js +18 -18
- package/dist/queue-advanced-model-switch.e2e.test.js +3 -4
- package/dist/queue-advanced-permissions-typing.e2e.test.js +2 -2
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +3 -3
- package/dist/queue-delete-message.e2e.test.js +4 -4
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +3 -3
- package/dist/queue-interrupt-drain.e2e.test.js +10 -10
- package/dist/queue-question-select-drain.e2e.test.js +5 -5
- package/dist/runtime-lifecycle.e2e.test.js +95 -11
- package/dist/schema.js +21 -0
- package/dist/session-handler/event-stream-state.js +240 -6
- package/dist/session-handler/event-stream-state.test.js +1116 -130
- package/dist/session-handler/thread-runtime-state.js +4 -0
- package/dist/session-handler/thread-session-runtime.js +499 -89
- package/dist/sleep.e2e.test.js +7 -7
- package/dist/store.js +1 -1
- package/dist/system-message.js +64 -23
- package/dist/system-message.test.js +69 -23
- package/dist/task-id-plugin.js +16 -0
- package/dist/task-runner.js +7 -0
- package/dist/task-schedule.test.js +1 -1
- package/dist/test-utils.js +5 -3
- package/dist/thread-message-queue.e2e.test.js +30 -31
- package/dist/undo-redo.e2e.test.js +3 -3
- package/dist/voice-message.e2e.test.js +64 -26
- package/dist/voice-routing-input.e2e.test.js +4 -4
- package/dist/voice-routing-question.e2e.test.js +4 -4
- package/dist/voice.js +107 -21
- package/dist/voice.test.js +52 -1
- package/dist/wait-session.js +24 -2
- package/dist/worktree-lifecycle.e2e.test.js +8 -8
- package/package.json +7 -7
- package/skills/egaki/SKILL.md +14 -0
- package/skills/holocron/SKILL.md +125 -42
- package/skills/opensrc/SKILL.md +20 -3
- package/skills/spiceflow/SKILL.md +53 -172
- package/skills/strada/SKILL.md +61 -232
- package/src/agent-model.e2e.test.ts +33 -33
- package/src/anthropic-account-identity.ts +1 -1
- package/src/anthropic-auth-plugin.test.ts +37 -2
- package/src/anthropic-auth-plugin.ts +9 -7
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +29 -0
- package/src/bash-tool-schema-plugin.ts +31 -0
- package/src/bin.ts +7 -3
- package/src/bot-lifecycle.test.ts +252 -0
- package/src/cache-drift-plugin.ts +34 -24
- package/src/cache-rewrite.test.ts +49 -0
- package/src/cache-rewrite.ts +96 -0
- package/src/channel-management.ts +144 -65
- package/src/cli-commands/misc.ts +4 -1
- package/src/cli-commands/project.ts +9 -4
- package/src/cli-commands/session.ts +167 -38
- package/src/cli-commands/thread.ts +86 -0
- package/src/cli-parsing.test.ts +3 -3
- package/src/cli-runner.ts +11 -0
- package/src/cli-send-thread.e2e.test.ts +2 -2
- package/src/cli.ts +8 -6
- package/src/commands/ask-question.ts +88 -25
- package/src/commands/queue.test.ts +19 -0
- package/src/commands/queue.ts +111 -45
- package/src/commands/restart-opencode-server.ts +12 -1
- package/src/commands/session-id.ts +5 -5
- package/src/database.ts +219 -6
- package/src/db.test.ts +236 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +2 -1
- package/src/discord-bot.ts +272 -185
- package/src/discord-command-registration.ts +3 -1
- package/src/external-directory-permissions.e2e.test.ts +3 -3
- package/src/external-directory-project-deny.e2e.test.ts +1 -1
- package/src/forum-sync/watchers.ts +20 -0
- package/src/gateway-proxy.e2e.test.ts +9 -9
- package/src/guild-categories.test.ts +260 -0
- package/src/hrana-server.ts +96 -16
- package/src/interaction-handler.ts +7 -0
- package/src/kimaki-opencode-plugin.test.ts +34 -0
- package/src/kimaki-opencode-plugin.ts +2 -0
- package/src/list-channel-threads.test.ts +90 -0
- package/src/list-channel-threads.ts +252 -0
- package/src/markdown.test.ts +108 -22
- package/src/markdown.ts +216 -103
- package/src/message-formatting.test.ts +129 -2
- package/src/message-formatting.ts +28 -14
- package/src/opencode.ts +28 -13
- package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
- package/src/question-abort-resume.e2e.test.ts +121 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +1 -1
- package/src/question-text-before-dropdown.e2e.test.ts +1 -1
- package/src/queue-advanced-abort.e2e.test.ts +3 -3
- package/src/queue-advanced-action-buttons.e2e.test.ts +5 -5
- package/src/queue-advanced-e2e-setup.ts +65 -0
- package/src/queue-advanced-footer.e2e.test.ts +18 -18
- package/src/queue-advanced-model-switch.e2e.test.ts +3 -4
- package/src/queue-advanced-permissions-typing.e2e.test.ts +2 -2
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +3 -3
- package/src/queue-delete-message.e2e.test.ts +4 -4
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +3 -3
- package/src/queue-interrupt-drain.e2e.test.ts +10 -10
- package/src/queue-question-select-drain.e2e.test.ts +5 -5
- package/src/runtime-lifecycle.e2e.test.ts +104 -10
- package/src/schema.sql +16 -0
- package/src/schema.ts +23 -0
- package/src/session-handler/event-stream-state.test.ts +1184 -133
- package/src/session-handler/event-stream-state.ts +322 -8
- package/src/session-handler/thread-runtime-state.ts +5 -0
- package/src/session-handler/thread-session-runtime.ts +597 -95
- package/src/sleep.e2e.test.ts +7 -7
- package/src/store.ts +2 -2
- package/src/system-message.test.ts +70 -23
- package/src/system-message.ts +67 -22
- package/src/task-id-plugin.ts +17 -0
- package/src/task-runner.ts +8 -0
- package/src/task-schedule.test.ts +1 -1
- package/src/test-utils.ts +5 -3
- package/src/thread-message-queue.e2e.test.ts +30 -31
- package/src/undo-redo.e2e.test.ts +3 -3
- package/src/voice-message.e2e.test.ts +70 -26
- package/src/voice-routing-input.e2e.test.ts +4 -4
- package/src/voice-routing-question.e2e.test.ts +4 -4
- package/src/voice.test.ts +56 -0
- package/src/voice.ts +135 -25
- package/src/wait-session.ts +25 -2
- package/src/worktree-lifecycle.e2e.test.ts +8 -8
|
@@ -399,9 +399,9 @@ describe('agent model resolution', () => {
|
|
|
399
399
|
"--- from: user (agent-model-tester)
|
|
400
400
|
Reply with exactly: agent-model-check
|
|
401
401
|
--- from: assistant (TestBot)
|
|
402
|
-
|
|
402
|
+
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
403
403
|
> ok
|
|
404
|
-
|
|
404
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
405
405
|
`);
|
|
406
406
|
expect(footerMessage).toBeDefined();
|
|
407
407
|
if (!footerMessage) {
|
|
@@ -443,9 +443,9 @@ describe('agent model resolution', () => {
|
|
|
443
443
|
"--- from: user (agent-model-tester)
|
|
444
444
|
Reply with exactly: system-context-check
|
|
445
445
|
--- from: assistant (TestBot)
|
|
446
|
-
|
|
446
|
+
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
447
447
|
> system-context-ok
|
|
448
|
-
|
|
448
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
449
449
|
`);
|
|
450
450
|
}, 15_000);
|
|
451
451
|
test('session.command path includes kimaki system prompt on first message', async () => {
|
|
@@ -549,9 +549,9 @@ describe('agent model resolution', () => {
|
|
|
549
549
|
"--- from: user (agent-model-tester)
|
|
550
550
|
Reply with exactly: channel-model-check
|
|
551
551
|
--- from: assistant (TestBot)
|
|
552
|
-
|
|
552
|
+
-# *using deterministic-provider/channel-model-v2*
|
|
553
553
|
> ok
|
|
554
|
-
|
|
554
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*"
|
|
555
555
|
`);
|
|
556
556
|
expect(footerMessage).toBeDefined();
|
|
557
557
|
if (!footerMessage) {
|
|
@@ -593,9 +593,9 @@ describe('agent model resolution', () => {
|
|
|
593
593
|
"--- from: user (agent-model-tester)
|
|
594
594
|
Reply with exactly: variant-check
|
|
595
595
|
--- from: assistant (TestBot)
|
|
596
|
-
|
|
596
|
+
-# *using deterministic-provider/channel-model-v2*
|
|
597
597
|
> ok
|
|
598
|
-
|
|
598
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*"
|
|
599
599
|
`);
|
|
600
600
|
expect(footer?.content).toContain(CHANNEL_MODEL);
|
|
601
601
|
expect(footer?.content).not.toContain(DEFAULT_MODEL);
|
|
@@ -662,7 +662,7 @@ describe('agent model resolution', () => {
|
|
|
662
662
|
Reusing context from <#SOURCE_THREAD> to answer prompt...
|
|
663
663
|
Reply with exactly: btw-model-check
|
|
664
664
|
> ok
|
|
665
|
-
|
|
665
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2*"
|
|
666
666
|
`);
|
|
667
667
|
expect(forkedSessionModel).toMatchInlineSnapshot(`
|
|
668
668
|
{
|
|
@@ -725,14 +725,14 @@ describe('agent model resolution', () => {
|
|
|
725
725
|
"--- from: user (agent-model-tester)
|
|
726
726
|
Reply with exactly: first-thread-msg
|
|
727
727
|
--- from: assistant (TestBot)
|
|
728
|
-
|
|
728
|
+
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
729
729
|
> ok
|
|
730
|
-
|
|
730
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
731
731
|
--- from: user (agent-model-tester)
|
|
732
732
|
Reply with exactly: second-thread-msg
|
|
733
733
|
--- from: assistant (TestBot)
|
|
734
734
|
> ok
|
|
735
|
-
|
|
735
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
736
736
|
`);
|
|
737
737
|
const secondMessages = await discord.thread(thread.id).getMessages();
|
|
738
738
|
const secondFooter = [...secondMessages]
|
|
@@ -779,9 +779,9 @@ describe('agent model resolution', () => {
|
|
|
779
779
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
780
780
|
"--- from: assistant (TestBot)
|
|
781
781
|
» **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-msg
|
|
782
|
-
|
|
782
|
+
-# *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
783
783
|
> ok
|
|
784
|
-
|
|
784
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
785
785
|
`);
|
|
786
786
|
}, 20_000);
|
|
787
787
|
test('/plan-agent with prompt persists a supported thinking variant', async () => {
|
|
@@ -815,9 +815,9 @@ describe('agent model resolution', () => {
|
|
|
815
815
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
816
816
|
"--- from: assistant (TestBot)
|
|
817
817
|
» **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-variant-msg
|
|
818
|
-
|
|
818
|
+
-# *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
819
819
|
> ok
|
|
820
|
-
|
|
820
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
821
821
|
`);
|
|
822
822
|
expect(sessionId ? await getSessionModel(sessionId) : undefined).toMatchInlineSnapshot(`
|
|
823
823
|
{
|
|
@@ -908,12 +908,12 @@ describe('agent model resolution', () => {
|
|
|
908
908
|
"--- from: user (agent-model-tester)
|
|
909
909
|
Reply with exactly: inline-existing-first-msg
|
|
910
910
|
--- from: assistant (TestBot)
|
|
911
|
-
|
|
911
|
+
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
912
912
|
> ok
|
|
913
|
-
|
|
913
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
914
914
|
» **agent-model-tester** (plan): Reply with exactly: inline-existing-plan-msg
|
|
915
915
|
> ok
|
|
916
|
-
|
|
916
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
917
917
|
`);
|
|
918
918
|
}, 20_000);
|
|
919
919
|
test('/plan-agent inside a thread switches the model for that thread', async () => {
|
|
@@ -962,9 +962,9 @@ describe('agent model resolution', () => {
|
|
|
962
962
|
"--- from: user (agent-model-tester)
|
|
963
963
|
Reply with exactly: switch-in-thread-msg
|
|
964
964
|
--- from: assistant (TestBot)
|
|
965
|
-
|
|
965
|
+
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
966
966
|
> ok
|
|
967
|
-
|
|
967
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
968
968
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
969
969
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
970
970
|
The agent will change on the next message.
|
|
@@ -972,7 +972,7 @@ describe('agent model resolution', () => {
|
|
|
972
972
|
Reply with exactly: after-switch-msg
|
|
973
973
|
--- from: assistant (TestBot)
|
|
974
974
|
> ok
|
|
975
|
-
|
|
975
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
976
976
|
`);
|
|
977
977
|
const secondFooter = [...(await discord.thread(thread.id).getMessages())]
|
|
978
978
|
.reverse()
|
|
@@ -1021,9 +1021,9 @@ describe('agent model resolution', () => {
|
|
|
1021
1021
|
"--- from: user (agent-model-tester)
|
|
1022
1022
|
Reply with exactly: race-switch-first-msg
|
|
1023
1023
|
--- from: assistant (TestBot)
|
|
1024
|
-
|
|
1024
|
+
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
1025
1025
|
> ok
|
|
1026
|
-
|
|
1026
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
1027
1027
|
--- from: user (agent-model-tester)
|
|
1028
1028
|
Reply with exactly: race-switch-follow-up
|
|
1029
1029
|
--- from: assistant (TestBot)
|
|
@@ -1031,7 +1031,7 @@ describe('agent model resolution', () => {
|
|
|
1031
1031
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
1032
1032
|
The agent will change on the next message.
|
|
1033
1033
|
> ok
|
|
1034
|
-
|
|
1034
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
1035
1035
|
`);
|
|
1036
1036
|
const secondFooter = [...(await discord.thread(thread.id).getMessages())]
|
|
1037
1037
|
.reverse()
|
|
@@ -1071,9 +1071,9 @@ describe('agent model resolution', () => {
|
|
|
1071
1071
|
"--- from: user (agent-model-tester)
|
|
1072
1072
|
Reply with exactly: race-channel-follow-up
|
|
1073
1073
|
--- from: assistant (TestBot)
|
|
1074
|
-
|
|
1074
|
+
-# *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
1075
1075
|
> ok
|
|
1076
|
-
|
|
1076
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
1077
1077
|
`);
|
|
1078
1078
|
const footer = [...(await discord.thread(thread.id).getMessages())]
|
|
1079
1079
|
.reverse()
|
|
@@ -1119,9 +1119,9 @@ describe('agent model resolution', () => {
|
|
|
1119
1119
|
"--- from: user (agent-model-tester)
|
|
1120
1120
|
Reply with exactly: refresh-agent-model-msg
|
|
1121
1121
|
--- from: assistant (TestBot)
|
|
1122
|
-
|
|
1122
|
+
-# *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
1123
1123
|
> ok
|
|
1124
|
-
|
|
1124
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***
|
|
1125
1125
|
Using **plan** agent for this session
|
|
1126
1126
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
1127
1127
|
The agent will change on the next message."
|
|
@@ -1161,9 +1161,9 @@ describe('agent model resolution', () => {
|
|
|
1161
1161
|
"--- from: user (agent-model-tester)
|
|
1162
1162
|
Reply with exactly: channel-vs-agent-msg
|
|
1163
1163
|
--- from: assistant (TestBot)
|
|
1164
|
-
|
|
1164
|
+
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
1165
1165
|
> ok
|
|
1166
|
-
|
|
1166
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
1167
1167
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
1168
1168
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
1169
1169
|
The agent will change on the next message."
|
|
@@ -1203,9 +1203,9 @@ describe('agent model resolution', () => {
|
|
|
1203
1203
|
"--- from: user (agent-model-tester)
|
|
1204
1204
|
Reply with exactly: plain-agent-override-msg
|
|
1205
1205
|
--- from: assistant (TestBot)
|
|
1206
|
-
|
|
1206
|
+
-# *using deterministic-provider/channel-model-v2*
|
|
1207
1207
|
> ok
|
|
1208
|
-
|
|
1208
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*
|
|
1209
1209
|
Switched to **plain** agent for this session
|
|
1210
1210
|
Model: *deterministic-provider/channel-model-v2* (channel override)
|
|
1211
1211
|
This model comes from a channel override. Use /model and press Clear override if you want this agent's model.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// LEGACY rotation, superseded by @subrouter/opencode. See oauth-rotation-shared.ts.
|
|
2
2
|
// Helpers for Anthropic OAuth account and Claude Code client identity.
|
|
3
|
-
const CLAUDE_CODE_USER_AGENT = 'claude-cli/2.1.
|
|
3
|
+
const CLAUDE_CODE_USER_AGENT = 'claude-cli/2.1.280 (external, cli)';
|
|
4
4
|
export function applyClaudeCodeRequestIdentity({ headers, accessToken, }) {
|
|
5
5
|
headers.set('authorization', `Bearer ${accessToken}`);
|
|
6
6
|
headers.set('user-agent', process.env.OPENCODE_ANTHROPIC_USER_AGENT || CLAUDE_CODE_USER_AGENT);
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { appendToastSessionMarker } from "./plugin-logger.js";
|
|
27
27
|
import { createPluginClient } from "./plugin-opencode-client.js";
|
|
28
|
-
import { isPermanentOAuthRefreshFailure, loadAccountStore, rememberAnthropicOAuth, removeAccountByAuth, rotateAnthropicAccount, saveAccountStore, setAnthropicAuth, shouldRotateAuth, upsertAccount, withAuthStateLock, } from "./anthropic-auth-state.js";
|
|
28
|
+
import { isPermanentOAuthRefreshFailure, loadAccountStore, rememberAnthropicOAuth, removeAccountByAuth, rotateAnthropicAccount, saveAccountStore, setAnthropicAuth, isOAuthStored, shouldRotateAuth, upsertAccount, withAuthStateLock, } from "./anthropic-auth-state.js";
|
|
29
29
|
import { applyClaudeCodeRequestIdentity, extractAnthropicAccountIdentity, } from "./anthropic-account-identity.js";
|
|
30
30
|
// PKCE (Proof Key for Code Exchange) using Web Crypto API.
|
|
31
31
|
// Reference: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/pkce.ts
|
|
@@ -717,9 +717,6 @@ function mergeBetas(existing, required) {
|
|
|
717
717
|
].join(",");
|
|
718
718
|
}
|
|
719
719
|
// --- Token refresh with dedup ---
|
|
720
|
-
function isOAuthStored(auth) {
|
|
721
|
-
return auth.type === "oauth";
|
|
722
|
-
}
|
|
723
720
|
/**
|
|
724
721
|
* Refresh Anthropic OAuth. On permanent refresh failure (invalid_grant),
|
|
725
722
|
* remove that account from the pool and retry with the next one.
|
|
@@ -727,6 +724,9 @@ function isOAuthStored(auth) {
|
|
|
727
724
|
*/
|
|
728
725
|
async function getFreshOAuth(getAuth, client, options) {
|
|
729
726
|
const auth = await getAuth();
|
|
727
|
+
if (auth == null) {
|
|
728
|
+
throw new Error("Anthropic OAuth credentials are missing. Re-login with /login and pick Claude Pro/Max.");
|
|
729
|
+
}
|
|
730
730
|
if (!isOAuthStored(auth))
|
|
731
731
|
return undefined;
|
|
732
732
|
if (auth.access && auth.expires > Date.now())
|
|
@@ -820,7 +820,7 @@ const AnthropicAuthPlugin = async ({ serverUrl, directory }) => {
|
|
|
820
820
|
provider: "anthropic",
|
|
821
821
|
async loader(getAuth, provider) {
|
|
822
822
|
const auth = await getAuth();
|
|
823
|
-
if (auth.type !== "oauth")
|
|
823
|
+
if (!auth || auth.type !== "oauth")
|
|
824
824
|
return {};
|
|
825
825
|
// Zero out costs for OAuth users (Claude Pro/Max subscription)
|
|
826
826
|
for (const model of Object.values(provider.models)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Tests Anthropic system prompt rewriting so project instructions survive OpenCode prompt layout changes.
|
|
2
2
|
import { describe, expect, test } from 'vitest';
|
|
3
3
|
import { applyClaudeCodeRequestIdentity } from './anthropic-account-identity.js';
|
|
4
|
-
import { replacer } from './anthropic-auth-plugin.js';
|
|
4
|
+
import { anthropicAuthPlugin, replacer } from './anthropic-auth-plugin.js';
|
|
5
5
|
async function transformSystem(systemText) {
|
|
6
6
|
const plugin = await replacer({});
|
|
7
7
|
const transform = plugin['experimental.chat.system.transform'];
|
|
@@ -13,6 +13,34 @@ async function transformSystem(systemText) {
|
|
|
13
13
|
}, output);
|
|
14
14
|
return output.system.join('\n');
|
|
15
15
|
}
|
|
16
|
+
describe('Anthropic OAuth loader', () => {
|
|
17
|
+
test('missing auth does not crash on auth.type', async () => {
|
|
18
|
+
const plugin = await anthropicAuthPlugin({
|
|
19
|
+
serverUrl: new URL('http://127.0.0.1:9'),
|
|
20
|
+
directory: '/tmp',
|
|
21
|
+
});
|
|
22
|
+
const loader = plugin.auth?.loader;
|
|
23
|
+
if (!loader)
|
|
24
|
+
throw new Error('missing loader');
|
|
25
|
+
let reads = 0;
|
|
26
|
+
const options = await loader((async () => {
|
|
27
|
+
reads += 1;
|
|
28
|
+
if (reads === 1) {
|
|
29
|
+
return {
|
|
30
|
+
type: 'oauth',
|
|
31
|
+
refresh: 'refresh-token',
|
|
32
|
+
access: 'access-token',
|
|
33
|
+
expires: Date.now() + 60_000,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}), { models: {} });
|
|
38
|
+
await expect(options.fetch?.('https://api.anthropic.com/v1/messages', {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
body: '{}',
|
|
41
|
+
})).rejects.toThrow('Anthropic OAuth credentials are missing');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
16
44
|
describe('Anthropic OAuth request identity', () => {
|
|
17
45
|
test('applies the current external Claude Code client headers', () => {
|
|
18
46
|
const headers = applyClaudeCodeRequestIdentity({
|
|
@@ -23,7 +51,7 @@ describe('Anthropic OAuth request identity', () => {
|
|
|
23
51
|
{
|
|
24
52
|
"accept": "application/json",
|
|
25
53
|
"authorization": "Bearer access-token",
|
|
26
|
-
"user-agent": "claude-cli/2.1.
|
|
54
|
+
"user-agent": "claude-cli/2.1.280 (external, cli)",
|
|
27
55
|
"x-app": "cli",
|
|
28
56
|
}
|
|
29
57
|
`);
|
|
@@ -9,7 +9,7 @@ import { homedir } from 'node:os';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { normalizeAnthropicAccountIdentity, } from './anthropic-account-identity.js';
|
|
11
11
|
import { accountLabel, authFilePath, findCurrentAccountIndex, isOAuthStored, isPermanentOAuthRefreshFailure, normalizeAccountStore, readJson, upsertAccount as sharedUpsertAccount, withAuthStateLock, writeJson, shouldRotateAuth, } from './oauth-rotation-shared.js';
|
|
12
|
-
export { accountLabel, authFilePath, withAuthStateLock, shouldRotateAuth, isPermanentOAuthRefreshFailure, };
|
|
12
|
+
export { accountLabel, authFilePath, isOAuthStored, withAuthStateLock, shouldRotateAuth, isPermanentOAuthRefreshFailure, };
|
|
13
13
|
// --- Store file path ---
|
|
14
14
|
export function accountsFilePath() {
|
|
15
15
|
if (process.env.XDG_DATA_HOME) {
|
|
@@ -3,7 +3,22 @@
|
|
|
3
3
|
// Assign jsonSchema only. Leave parameters as the Effect Schema used to decode
|
|
4
4
|
// args before execute. Extra fields stay in the model-facing schema and in
|
|
5
5
|
// Discord input; they never reach the shell.
|
|
6
|
+
//
|
|
7
|
+
// Also injects KIMAKI_SESSION_ID into bash env. /btw and /fork clone parent
|
|
8
|
+
// history, so the copied system prompt still says --session <parent>. Upload
|
|
9
|
+
// then prefers this live env over that stale flag.
|
|
6
10
|
import { z } from 'zod';
|
|
11
|
+
export const KIMAKI_SESSION_ID_ENV = 'KIMAKI_SESSION_ID';
|
|
12
|
+
export function injectKimakiSessionEnv({ sessionID, env, }) {
|
|
13
|
+
if (!sessionID)
|
|
14
|
+
return;
|
|
15
|
+
env[KIMAKI_SESSION_ID_ENV] = sessionID;
|
|
16
|
+
}
|
|
17
|
+
export function resolveUploadToDiscordSessionId({ flagSessionId, envSessionId = process.env[KIMAKI_SESSION_ID_ENV], } = {}) {
|
|
18
|
+
if (envSessionId)
|
|
19
|
+
return envSessionId;
|
|
20
|
+
return flagSessionId;
|
|
21
|
+
}
|
|
7
22
|
export const bashParameters = z.object({
|
|
8
23
|
command: z.string().describe('The command to execute'),
|
|
9
24
|
description: z
|
|
@@ -28,5 +43,8 @@ export const bashToolSchemaPlugin = async () => {
|
|
|
28
43
|
return;
|
|
29
44
|
extendBashToolDefinition(output);
|
|
30
45
|
},
|
|
46
|
+
'shell.env': async (input, output) => {
|
|
47
|
+
injectKimakiSessionEnv({ sessionID: input.sessionID, env: output.env });
|
|
48
|
+
},
|
|
31
49
|
};
|
|
32
50
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import { extendBashToolDefinition, } from './bash-tool-schema-plugin.js';
|
|
2
|
+
import { extendBashToolDefinition, injectKimakiSessionEnv, KIMAKI_SESSION_ID_ENV, resolveUploadToDiscordSessionId, } from './bash-tool-schema-plugin.js';
|
|
3
3
|
describe('extendBashToolDefinition', () => {
|
|
4
4
|
test('keeps parameters and assigns jsonSchema from bashParameters', () => {
|
|
5
5
|
const parameters = { keep: true };
|
|
@@ -47,3 +47,22 @@ describe('extendBashToolDefinition', () => {
|
|
|
47
47
|
`);
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
|
+
describe('upload-to-discord session targeting', () => {
|
|
51
|
+
test('bash env uses the live OpenCode session, not a copied --session flag', () => {
|
|
52
|
+
const env = {};
|
|
53
|
+
injectKimakiSessionEnv({ sessionID: 'ses_child', env });
|
|
54
|
+
expect(env).toEqual({ [KIMAKI_SESSION_ID_ENV]: 'ses_child' });
|
|
55
|
+
expect(resolveUploadToDiscordSessionId({
|
|
56
|
+
flagSessionId: 'ses_parent',
|
|
57
|
+
envSessionId: env[KIMAKI_SESSION_ID_ENV],
|
|
58
|
+
})).toBe('ses_child');
|
|
59
|
+
});
|
|
60
|
+
test('falls back to --session when bash is not inside an OpenCode session', () => {
|
|
61
|
+
expect(resolveUploadToDiscordSessionId({
|
|
62
|
+
flagSessionId: 'ses_parent',
|
|
63
|
+
})).toBe('ses_parent');
|
|
64
|
+
});
|
|
65
|
+
test('returns undefined when neither live session nor flag is set', () => {
|
|
66
|
+
expect(resolveUploadToDiscordSessionId({})).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
package/dist/bin.js
CHANGED
|
@@ -91,8 +91,11 @@ else {
|
|
|
91
91
|
`--diagnostic-dir=${HEAP_SNAPSHOT_DIR}`,
|
|
92
92
|
];
|
|
93
93
|
const args = [...heapArgs, ...process.execArgv, ...process.argv.slice(1)];
|
|
94
|
+
// The IPC channel is only a liveness link: when this wrapper dies (even by
|
|
95
|
+
// SIGKILL) the child sees 'disconnect' and shuts down instead of staying
|
|
96
|
+
// orphaned on the lock port. See registerBotLifecycleHandlers.
|
|
94
97
|
const currentChild = spawn(process.argv[0], args, {
|
|
95
|
-
stdio: 'inherit',
|
|
98
|
+
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
96
99
|
env: { ...process.env, __KIMAKI_CHILD: '1' },
|
|
97
100
|
});
|
|
98
101
|
child = currentChild;
|
|
@@ -130,8 +133,9 @@ else {
|
|
|
130
133
|
});
|
|
131
134
|
}
|
|
132
135
|
// Forward signals to child so graceful shutdown and heap snapshots work.
|
|
133
|
-
//
|
|
134
|
-
|
|
136
|
+
// Termination signals mark shutdownRequested so we don't restart after
|
|
137
|
+
// graceful exit. SIGHUP comes from closing the terminal.
|
|
138
|
+
for (const sig of ['SIGTERM', 'SIGINT', 'SIGHUP']) {
|
|
135
139
|
process.on(sig, () => {
|
|
136
140
|
shutdownRequested = true;
|
|
137
141
|
killChild(sig);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Real-process tests for bot shutdown and lock-port recovery.
|
|
2
|
+
// Regression: Ctrl+C during startup left the bot alive on the hrana lock port
|
|
3
|
+
// (29988), and every next start failed with "still in use after eviction".
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
|
+
import dedent from 'string-dedent';
|
|
9
|
+
import * as errore from 'errore';
|
|
10
|
+
import { afterEach, describe, expect, test } from 'vitest';
|
|
11
|
+
import { evictExistingInstance } from './hrana-server.js';
|
|
12
|
+
const srcDir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const hranaModule = pathToFileURL(path.join(srcDir, 'hrana-server.ts')).href;
|
|
14
|
+
const botModule = pathToFileURL(path.join(srcDir, 'discord-bot.ts')).href;
|
|
15
|
+
const children = [];
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
for (const child of children.splice(0)) {
|
|
18
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
19
|
+
child.kill('SIGKILL');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
async function getFreePort() {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const srv = net.createServer();
|
|
26
|
+
srv.once('error', reject);
|
|
27
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
28
|
+
const address = srv.address();
|
|
29
|
+
if (!address || typeof address === 'string') {
|
|
30
|
+
reject(new Error('No port assigned'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
srv.close(() => resolve(address.port));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function isPortFree(port) {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const srv = net.createServer();
|
|
40
|
+
srv.once('error', () => resolve(false));
|
|
41
|
+
srv.listen(port, '127.0.0.1', () => {
|
|
42
|
+
srv.close(() => resolve(true));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// Runs an ESM script through tsx so it can import the real .ts sources.
|
|
47
|
+
function startFixture({ script, port, ipc = false, env = {}, }) {
|
|
48
|
+
const child = spawn(process.execPath, ['--import', 'tsx', '--input-type=module', '-e', script], {
|
|
49
|
+
cwd: path.dirname(srcDir),
|
|
50
|
+
env: {
|
|
51
|
+
...process.env,
|
|
52
|
+
KIMAKI_LOCK_PORT: String(port),
|
|
53
|
+
__KIMAKI_CHILD: ipc ? '1' : undefined,
|
|
54
|
+
...env,
|
|
55
|
+
},
|
|
56
|
+
stdio: ipc ? ['ignore', 'pipe', 'pipe', 'ipc'] : ['ignore', 'pipe', 'pipe'],
|
|
57
|
+
});
|
|
58
|
+
children.push(child);
|
|
59
|
+
let output = '';
|
|
60
|
+
const ready = new Promise((resolve, reject) => {
|
|
61
|
+
child.stdout.on('data', (chunk) => {
|
|
62
|
+
output += chunk.toString();
|
|
63
|
+
if (output.includes('FIXTURE_READY'))
|
|
64
|
+
resolve();
|
|
65
|
+
});
|
|
66
|
+
child.stderr.on('data', (chunk) => {
|
|
67
|
+
output += chunk.toString();
|
|
68
|
+
});
|
|
69
|
+
child.once('exit', (code, signal) => {
|
|
70
|
+
reject(new Error(`fixture exited early (${code ?? signal}): ${output}`));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
const exited = new Promise((resolve) => {
|
|
74
|
+
child.once('exit', (code, signal) => resolve({ code, signal }));
|
|
75
|
+
});
|
|
76
|
+
return { child, ready, exited, output: () => output };
|
|
77
|
+
}
|
|
78
|
+
function hranaScript({ extra = '', afterReady = '', }) {
|
|
79
|
+
return dedent `
|
|
80
|
+
import os from 'node:os'
|
|
81
|
+
import path from 'node:path'
|
|
82
|
+
import fs from 'node:fs'
|
|
83
|
+
import { startHranaServer } from '${hranaModule}'
|
|
84
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kimaki-lifecycle-'))
|
|
85
|
+
const result = await startHranaServer({ dbPath: path.join(dir, 'db.sqlite') })
|
|
86
|
+
if (result instanceof Error) throw result
|
|
87
|
+
${extra}
|
|
88
|
+
// Same situation as the opencode.ts fallback: a listener that never exits.
|
|
89
|
+
process.on('SIGINT', () => {})
|
|
90
|
+
process.on('SIGTERM', () => {})
|
|
91
|
+
console.log('FIXTURE_READY')
|
|
92
|
+
${afterReady}
|
|
93
|
+
setInterval(() => {}, 1000)
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
// Minimal stand-in for bin.ts: IPC child, respawn on crash, no respawn after
|
|
97
|
+
// SIGTERM. Enough to prove eviction targets the wrapper, not just the child.
|
|
98
|
+
const WRAPPER_SCRIPT = dedent `
|
|
99
|
+
import { spawn } from 'node:child_process'
|
|
100
|
+
let stopping = false
|
|
101
|
+
let child
|
|
102
|
+
process.on('SIGTERM', () => {
|
|
103
|
+
stopping = true
|
|
104
|
+
child.kill('SIGTERM')
|
|
105
|
+
})
|
|
106
|
+
function start() {
|
|
107
|
+
child = spawn(
|
|
108
|
+
process.execPath,
|
|
109
|
+
['--import', 'tsx', '--input-type=module', '-e', process.env.CHILD_SCRIPT],
|
|
110
|
+
{ stdio: ['ignore', 'inherit', 'inherit', 'ipc'], env: { ...process.env, __KIMAKI_CHILD: '1' } },
|
|
111
|
+
)
|
|
112
|
+
console.log('CHILD_PID=' + child.pid)
|
|
113
|
+
child.on('exit', () => {
|
|
114
|
+
if (stopping) process.exit(0)
|
|
115
|
+
console.log('RESPAWNED')
|
|
116
|
+
start()
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
start()
|
|
120
|
+
`;
|
|
121
|
+
function isPidAlive(pid) {
|
|
122
|
+
const result = errore.try(() => {
|
|
123
|
+
process.kill(pid, 0);
|
|
124
|
+
});
|
|
125
|
+
return !(result instanceof Error);
|
|
126
|
+
}
|
|
127
|
+
describe('bot lifecycle', () => {
|
|
128
|
+
test('eviction SIGKILLs an old instance that ignores SIGTERM', async () => {
|
|
129
|
+
const port = await getFreePort();
|
|
130
|
+
const fixture = startFixture({ script: hranaScript({ extra: '' }), port });
|
|
131
|
+
await fixture.ready;
|
|
132
|
+
expect(await isPortFree(port)).toBe(false);
|
|
133
|
+
await evictExistingInstance({ port, gracePeriodMs: 500 });
|
|
134
|
+
expect(await fixture.exited).toEqual({ code: null, signal: 'SIGKILL' });
|
|
135
|
+
expect(await isPortFree(port)).toBe(true);
|
|
136
|
+
}, 20_000);
|
|
137
|
+
test('eviction stops the old wrapper so it cannot respawn the child', async () => {
|
|
138
|
+
const port = await getFreePort();
|
|
139
|
+
const wrapper = startFixture({
|
|
140
|
+
script: WRAPPER_SCRIPT,
|
|
141
|
+
port,
|
|
142
|
+
env: { CHILD_SCRIPT: hranaScript({}) },
|
|
143
|
+
});
|
|
144
|
+
await wrapper.ready;
|
|
145
|
+
const childPid = Number(/CHILD_PID=(\d+)/.exec(wrapper.output())?.[1]);
|
|
146
|
+
// The child ignores SIGTERM, so the wrapper's forwarded SIGTERM is not
|
|
147
|
+
// enough and eviction must SIGKILL both, wrapper first.
|
|
148
|
+
await evictExistingInstance({ port, gracePeriodMs: 500 });
|
|
149
|
+
expect(await wrapper.exited).toEqual({ code: null, signal: 'SIGKILL' });
|
|
150
|
+
expect(isPidAlive(childPid)).toBe(false);
|
|
151
|
+
expect(wrapper.output()).not.toContain('RESPAWNED');
|
|
152
|
+
expect(await isPortFree(port)).toBe(true);
|
|
153
|
+
}, 20_000);
|
|
154
|
+
test('SIGINT during startup releases the lock port', async () => {
|
|
155
|
+
const port = await getFreePort();
|
|
156
|
+
const fixture = startFixture({
|
|
157
|
+
script: hranaScript({
|
|
158
|
+
extra: dedent `
|
|
159
|
+
const { registerBotLifecycleHandlers } = await import('${botModule}')
|
|
160
|
+
registerBotLifecycleHandlers()
|
|
161
|
+
`,
|
|
162
|
+
}),
|
|
163
|
+
port,
|
|
164
|
+
});
|
|
165
|
+
await fixture.ready;
|
|
166
|
+
fixture.child.kill('SIGINT');
|
|
167
|
+
expect(await fixture.exited).toEqual({ code: 0, signal: null });
|
|
168
|
+
expect(await isPortFree(port)).toBe(true);
|
|
169
|
+
}, 30_000);
|
|
170
|
+
test('child shuts down when the wrapper IPC channel closes', async () => {
|
|
171
|
+
const port = await getFreePort();
|
|
172
|
+
const fixture = startFixture({
|
|
173
|
+
script: hranaScript({
|
|
174
|
+
extra: dedent `
|
|
175
|
+
const { registerBotLifecycleHandlers } = await import('${botModule}')
|
|
176
|
+
registerBotLifecycleHandlers()
|
|
177
|
+
`,
|
|
178
|
+
}),
|
|
179
|
+
port,
|
|
180
|
+
ipc: true,
|
|
181
|
+
});
|
|
182
|
+
await fixture.ready;
|
|
183
|
+
// Same as the wrapper dying: the OS closes the IPC pipe.
|
|
184
|
+
fixture.child.disconnect();
|
|
185
|
+
expect(await fixture.exited).toEqual({ code: 0, signal: null });
|
|
186
|
+
expect(await isPortFree(port)).toBe(true);
|
|
187
|
+
}, 30_000);
|
|
188
|
+
test('child shuts down if the wrapper died before handlers were registered', async () => {
|
|
189
|
+
const port = await getFreePort();
|
|
190
|
+
const fixture = startFixture({
|
|
191
|
+
script: hranaScript({
|
|
192
|
+
afterReady: dedent `
|
|
193
|
+
await new Promise((resolve) => process.once('disconnect', resolve))
|
|
194
|
+
const { registerBotLifecycleHandlers } = await import('${botModule}')
|
|
195
|
+
registerBotLifecycleHandlers()
|
|
196
|
+
`,
|
|
197
|
+
}),
|
|
198
|
+
port,
|
|
199
|
+
ipc: true,
|
|
200
|
+
});
|
|
201
|
+
await fixture.ready;
|
|
202
|
+
fixture.child.disconnect();
|
|
203
|
+
expect(await fixture.exited).toEqual({ code: 0, signal: null });
|
|
204
|
+
expect(await isPortFree(port)).toBe(true);
|
|
205
|
+
}, 30_000);
|
|
206
|
+
});
|