@@ -5,7 +5,13 @@ import os from "node:os";
55import path from "node:path" ;
66import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
77import { setActivePluginRegistry } from "../../plugins/runtime.js" ;
8+ import type { DB as OpenClawStateKyselyDatabase } from "../../state/openclaw-state-db.generated.js" ;
9+ import {
10+ closeOpenClawStateDatabaseForTest ,
11+ runOpenClawStateWriteTransaction ,
12+ } from "../../state/openclaw-state-db.js" ;
813import { createTestRegistry } from "../../test-utils/channel-plugins.js" ;
14+ import { executeSqliteQuerySync , getNodeSqliteKysely } from "../kysely-sync.js" ;
915import {
1016 testing ,
1117 bindGenericCurrentConversation ,
@@ -17,6 +23,11 @@ import {
1723} from "./current-conversation-bindings.js" ;
1824import type { SessionBindingRecord } from "./session-binding.types.js" ;
1925
26+ type CurrentConversationBindingDatabase = Pick <
27+ OpenClawStateKyselyDatabase ,
28+ "current_conversation_bindings"
29+ > ;
30+
2031function expectSessionBinding ( bound : SessionBindingRecord | null ) : SessionBindingRecord {
2132 if ( bound === null ) {
2233 throw new Error ( "Expected current-conversation binding" ) ;
@@ -45,6 +56,40 @@ function expectBindingMetadata(
4556 }
4657}
4758
59+ function buildConversationKey ( ref : SessionBindingRecord [ "conversation" ] ) : string {
60+ return [ ref . channel , ref . accountId , ref . parentConversationId ?? "" , ref . conversationId ] . join (
61+ "\u241f" ,
62+ ) ;
63+ }
64+
65+ function seedPersistedBinding ( record : SessionBindingRecord ) : void {
66+ runOpenClawStateWriteTransaction ( ( { db } ) => {
67+ const bindingDb = getNodeSqliteKysely < CurrentConversationBindingDatabase > ( db ) ;
68+ executeSqliteQuerySync (
69+ db ,
70+ bindingDb . insertInto ( "current_conversation_bindings" ) . values ( {
71+ binding_key : buildConversationKey ( record . conversation ) ,
72+ binding_id : record . bindingId ,
73+ target_agent_id : "codex" ,
74+ target_session_id : null ,
75+ target_session_key : record . targetSessionKey ,
76+ channel : record . conversation . channel ,
77+ account_id : record . conversation . accountId ,
78+ conversation_kind : "current" ,
79+ parent_conversation_id : record . conversation . parentConversationId ?? null ,
80+ conversation_id : record . conversation . conversationId ,
81+ target_kind : record . targetKind ,
82+ status : record . status ,
83+ bound_at : record . boundAt ,
84+ expires_at : record . expiresAt ?? null ,
85+ metadata_json : record . metadata ? JSON . stringify ( record . metadata ) : null ,
86+ record_json : JSON . stringify ( record ) ,
87+ updated_at : record . boundAt ,
88+ } ) ,
89+ ) ;
90+ } ) ;
91+ }
92+
4893function setMinimalCurrentConversationRegistry ( ) : void {
4994 setActivePluginRegistry (
5095 createTestRegistry ( [
@@ -82,6 +127,7 @@ describe("generic current-conversation bindings", () => {
82127 testing . resetCurrentConversationBindingsForTests ( {
83128 deletePersistedFile : true ,
84129 } ) ;
130+ closeOpenClawStateDatabaseForTest ( ) ;
85131 if ( previousStateDir == null ) {
86132 delete process . env . OPENCLAW_STATE_DIR ;
87133 } else {
@@ -155,31 +201,21 @@ describe("generic current-conversation bindings", () => {
155201 } ) ;
156202
157203 it ( "normalizes persisted target session keys on reload" , async ( ) => {
158- const filePath = testing . resolveBindingsFilePath ( ) ;
159- await fs . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
160- await fs . writeFile (
161- filePath ,
162- JSON . stringify ( {
163- version : 1 ,
164- bindings : [
165- {
166- bindingId : "generic:workspace\u241fdefault\u241f\u241fuser:U123" ,
167- targetSessionKey : " agent:codex:acp:workspace-dm " ,
168- targetKind : "session" ,
169- conversation : {
170- channel : "workspace" ,
171- accountId : "default" ,
172- conversationId : "user:U123" ,
173- } ,
174- status : "active" ,
175- boundAt : 1234 ,
176- metadata : {
177- label : "workspace-dm" ,
178- } ,
179- } ,
180- ] ,
181- } ) ,
182- ) ;
204+ seedPersistedBinding ( {
205+ bindingId : "generic:workspace\u241fdefault\u241f\u241fuser:U123" ,
206+ targetSessionKey : " agent:codex:acp:workspace-dm " ,
207+ targetKind : "session" ,
208+ conversation : {
209+ channel : "workspace" ,
210+ accountId : "default" ,
211+ conversationId : "user:U123" ,
212+ } ,
213+ status : "active" ,
214+ boundAt : 1234 ,
215+ metadata : {
216+ label : "workspace-dm" ,
217+ } ,
218+ } ) ;
183219
184220 const resolved = resolveGenericCurrentConversationBinding ( {
185221 channel : "workspace" ,
@@ -237,32 +273,22 @@ describe("generic current-conversation bindings", () => {
237273 } ) ;
238274
239275 it ( "migrates persisted legacy self-parent binding ids on load" , async ( ) => {
240- const filePath = testing . resolveBindingsFilePath ( ) ;
241- await fs . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
242- await fs . writeFile (
243- filePath ,
244- JSON . stringify ( {
245- version : 1 ,
246- bindings : [
247- {
248- bindingId : "generic:forum\u241fdefault\u241f6098642967\u241f6098642967" ,
249- targetSessionKey : "agent:codex:acp:forum-dm" ,
250- targetKind : "session" ,
251- conversation : {
252- channel : "forum" ,
253- accountId : "default" ,
254- conversationId : "6098642967" ,
255- parentConversationId : "6098642967" ,
256- } ,
257- status : "active" ,
258- boundAt : 1234 ,
259- metadata : {
260- label : "forum-dm" ,
261- } ,
262- } ,
263- ] ,
264- } ) ,
265- ) ;
276+ seedPersistedBinding ( {
277+ bindingId : "generic:forum\u241fdefault\u241f6098642967\u241f6098642967" ,
278+ targetSessionKey : "agent:codex:acp:forum-dm" ,
279+ targetKind : "session" ,
280+ conversation : {
281+ channel : "forum" ,
282+ accountId : "default" ,
283+ conversationId : "6098642967" ,
284+ parentConversationId : "6098642967" ,
285+ } ,
286+ status : "active" ,
287+ boundAt : 1234 ,
288+ metadata : {
289+ label : "forum-dm" ,
290+ } ,
291+ } ) ;
266292
267293 const resolved = resolveGenericCurrentConversationBinding ( {
268294 channel : "forum" ,
@@ -328,29 +354,19 @@ describe("generic current-conversation bindings", () => {
328354 } ) ;
329355
330356 it ( "drops persisted bindings with invalid expiration timestamps" , async ( ) => {
331- const filePath = testing . resolveBindingsFilePath ( ) ;
332- await fs . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
333- await fs . writeFile (
334- filePath ,
335- JSON . stringify ( {
336- version : 1 ,
337- bindings : [
338- {
339- bindingId : "generic:workspace\u241fdefault\u241f\u241fuser:U123" ,
340- targetSessionKey : "agent:codex:acp:workspace-dm" ,
341- targetKind : "session" ,
342- conversation : {
343- channel : "workspace" ,
344- accountId : "default" ,
345- conversationId : "user:U123" ,
346- } ,
347- status : "active" ,
348- boundAt : 1234 ,
349- expiresAt : 8_640_000_000_000_001 ,
350- } ,
351- ] ,
352- } ) ,
353- ) ;
357+ seedPersistedBinding ( {
358+ bindingId : "generic:workspace\u241fdefault\u241f\u241fuser:U123" ,
359+ targetSessionKey : "agent:codex:acp:workspace-dm" ,
360+ targetKind : "session" ,
361+ conversation : {
362+ channel : "workspace" ,
363+ accountId : "default" ,
364+ conversationId : "user:U123" ,
365+ } ,
366+ status : "active" ,
367+ boundAt : 1234 ,
368+ expiresAt : 8_640_000_000_000_001 ,
369+ } ) ;
354370
355371 expect (
356372 resolveGenericCurrentConversationBinding ( {
0 commit comments