11import { describe , expect , it } from "@effect/vitest" ;
22import { Cause , Effect , Exit , Schema } from "effect" ;
33
4- import { StorageError } from "./fuma-runtime" ;
4+ import { createExecutor } from "./executor" ;
5+ import { StorageError , type FumaDb } from "./fuma-runtime" ;
56import { Owner } from "./ids" ;
67import { definePlugin } from "./plugin" ;
78import {
@@ -10,7 +11,7 @@ import {
1011 type PluginStorageCollectionQueryInput ,
1112 type PluginStorageCollectionWhere ,
1213} from "./plugin-storage" ;
13- import { makeTestExecutor } from "./testing" ;
14+ import { makeTestConfig , makeTestExecutor } from "./testing" ;
1415
1516const ToolCall = Schema . Struct ( {
1617 runId : Schema . String ,
@@ -109,6 +110,48 @@ const call = (input: {
109110 durationMs : input . durationMs ?? 0 ,
110111} ) ;
111112
113+ const failPluginStorageBulkWriteAfterFirstRow = ( db : FumaDb ) : FumaDb => {
114+ const wrap = ( source : FumaDb , failBulkWrite : boolean ) : FumaDb =>
115+ new Proxy ( source , {
116+ get ( target , property , receiver ) {
117+ if ( property === "withContext" ) {
118+ const withContext = target . withContext ;
119+ return withContext === undefined
120+ ? undefined
121+ : ( context : unknown ) => wrap ( withContext ( context ) , failBulkWrite ) ;
122+ }
123+ if ( property === "transaction" ) {
124+ const transaction : FumaDb [ "transaction" ] = ( run ) =>
125+ target . transaction ( ( transactionDb ) => run ( wrap ( transactionDb , true ) ) ) ;
126+ return transaction ;
127+ }
128+ if ( property === "upsertMany" && failBulkWrite ) {
129+ const upsertMany : FumaDb [ "upsertMany" ] = async ( table , options ) => {
130+ if ( table !== "plugin_storage" || options . values . length < 2 ) {
131+ return target . upsertMany ( table , options ) ;
132+ }
133+
134+ await target . upsertMany ( table , {
135+ ...options ,
136+ values : options . values . slice ( 0 , 1 ) ,
137+ } ) ;
138+ // oxlint-disable-next-line executor/no-promise-reject -- boundary: fault-injecting FumaDB adapter must reject to exercise transaction rollback
139+ return Promise . reject (
140+ new StorageError ( {
141+ message : "Injected plugin storage bulk-write failure." ,
142+ cause : undefined ,
143+ } ) ,
144+ ) ;
145+ } ;
146+ return upsertMany ;
147+ }
148+ return Reflect . get ( target , property , receiver ) ;
149+ } ,
150+ } ) ;
151+
152+ return wrap ( db , false ) ;
153+ } ;
154+
112155describe ( "plugin storage collections" , ( ) => {
113156 it . effect ( "queries declared indexes through the executor's SQLite FumaDB target" , ( ) =>
114157 Effect . gen ( function * ( ) {
@@ -218,6 +261,57 @@ describe("plugin storage collections", () => {
218261 } ) ,
219262 ) ;
220263
264+ it . effect ( "rolls back every plugin storage row when a bulk write fails" , ( ) =>
265+ Effect . gen ( function * ( ) {
266+ const config = makeTestConfig ( {
267+ backend : "sqlite" ,
268+ plugins : [ executionHistoryPlugin ] as const ,
269+ } ) ;
270+ const executor = yield * Effect . acquireRelease (
271+ createExecutor ( {
272+ ...config ,
273+ db : failPluginStorageBulkWriteAfterFirstRow ( config . db ) ,
274+ } ) ,
275+ ( instance ) =>
276+ instance
277+ . close ( )
278+ . pipe (
279+ Effect . ignore ,
280+ Effect . andThen ( Effect . promise ( ( ) => config . testDb . close ( ) ) . pipe ( Effect . ignore ) ) ,
281+ ) ,
282+ ) ;
283+
284+ const exit = yield * Effect . exit (
285+ executor . executionHistory . recordMany ( "org" , [
286+ {
287+ key : "call-first" ,
288+ data : call ( {
289+ runId : "run-rollback" ,
290+ toolId : "browser" ,
291+ status : "ok" ,
292+ startedAt : "2026-05-29T12:00:00.000Z" ,
293+ } ) ,
294+ } ,
295+ {
296+ key : "call-second" ,
297+ data : call ( {
298+ runId : "run-rollback" ,
299+ toolId : "shell" ,
300+ status : "ok" ,
301+ startedAt : "2026-05-29T12:01:00.000Z" ,
302+ } ) ,
303+ } ,
304+ ] ) ,
305+ ) ;
306+ expect ( Exit . isFailure ( exit ) ) . toBe ( true ) ;
307+
308+ const stored = yield * executor . executionHistory . query ( {
309+ where : { runId : "run-rollback" } ,
310+ } ) ;
311+ expect ( stored ) . toEqual ( [ ] ) ;
312+ } ) ,
313+ ) ;
314+
221315 it . effect (
222316 "bulk puts large plugin storage row sets in bounded batches" ,
223317 ( ) =>
0 commit comments