@swarmclawai/swarmclaw 0.9.4 → 0.9.6
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/README.md +8 -8
- package/package.json +1 -1
- package/src/app/api/settings/route.ts +1 -0
- package/src/app/api/wallets/[id]/send/route.ts +10 -4
- package/src/app/api/wallets/route.ts +5 -2
- package/src/app/settings/page.tsx +9 -0
- package/src/components/wallets/wallet-panel.tsx +12 -1
- package/src/components/wallets/wallet-section.tsx +4 -1
- package/src/lib/server/agents/main-agent-loop-advanced.test.ts +35 -0
- package/src/lib/server/agents/main-agent-loop.ts +12 -1
- package/src/lib/server/agents/subagent-runtime.test.ts +99 -16
- package/src/lib/server/agents/subagent-runtime.ts +115 -19
- package/src/lib/server/agents/subagent-swarm.ts +3 -3
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +112 -0
- package/src/lib/server/chat-execution/chat-execution.ts +357 -152
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +51 -0
- package/src/lib/server/chat-execution/stream-agent-chat.ts +201 -38
- package/src/lib/server/chat-execution/stream-continuation.ts +46 -0
- package/src/lib/server/connectors/contact-boundaries.ts +70 -8
- package/src/lib/server/connectors/manager.test.ts +129 -7
- package/src/lib/server/plugins.test.ts +263 -0
- package/src/lib/server/plugins.ts +406 -10
- package/src/lib/server/session-tools/context.ts +15 -1
- package/src/lib/server/session-tools/index.ts +42 -6
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +50 -0
- package/src/lib/server/session-tools/subagent.ts +3 -3
- package/src/lib/server/tool-loop-detection.test.ts +21 -0
- package/src/lib/server/tool-loop-detection.ts +79 -0
- package/src/lib/server/wallet/wallet-service.test.ts +25 -1
- package/src/lib/server/wallet/wallet-service.ts +13 -0
- package/src/types/index.ts +134 -1
- package/src/views/settings/section-wallets.tsx +35 -0
|
@@ -994,7 +994,7 @@ describe('sanitizeConnectorOutboundContent', () => {
|
|
|
994
994
|
storage.saveAgents({
|
|
995
995
|
agent_1: {
|
|
996
996
|
id: 'agent_1',
|
|
997
|
-
name: '
|
|
997
|
+
name: 'Nova',
|
|
998
998
|
provider: 'test-provider',
|
|
999
999
|
model: 'test-model',
|
|
1000
1000
|
plugins: [],
|
|
@@ -1020,7 +1020,7 @@ describe('sanitizeConnectorOutboundContent', () => {
|
|
|
1020
1020
|
storage.saveSessions({
|
|
1021
1021
|
agent_thread: {
|
|
1022
1022
|
id: 'agent_thread',
|
|
1023
|
-
name: '
|
|
1023
|
+
name: 'Nova',
|
|
1024
1024
|
cwd: process.env.WORKSPACE_DIR,
|
|
1025
1025
|
user: 'default',
|
|
1026
1026
|
provider: 'test-provider',
|
|
@@ -1040,16 +1040,16 @@ describe('sanitizeConnectorOutboundContent', () => {
|
|
|
1040
1040
|
sessionId: null,
|
|
1041
1041
|
category: 'identity/preferences',
|
|
1042
1042
|
title: 'Wife communication rule',
|
|
1043
|
-
content: '
|
|
1043
|
+
content: 'Riley\\'s partner (+44 7700 900111): Do NOT respond unless she addresses Nova directly or mentions Nova directly. If unsure, verify whether the message is meant for the agent before responding.',
|
|
1044
1044
|
})
|
|
1045
1045
|
|
|
1046
1046
|
const connector = storage.loadConnectors().conn_1
|
|
1047
1047
|
const response = await manager.routeConnectorMessageForTest(connector, {
|
|
1048
1048
|
platform: 'whatsapp',
|
|
1049
|
-
channelId: '
|
|
1050
|
-
senderId: '
|
|
1051
|
-
senderName: '
|
|
1052
|
-
text: 'Dinner is ready for
|
|
1049
|
+
channelId: '447700900111@s.whatsapp.net',
|
|
1050
|
+
senderId: '447700900111@s.whatsapp.net',
|
|
1051
|
+
senderName: 'Riley Partner',
|
|
1052
|
+
text: 'Dinner is ready for Riley.',
|
|
1053
1053
|
messageId: 'in-quiet-1',
|
|
1054
1054
|
isGroup: false,
|
|
1055
1055
|
})
|
|
@@ -1070,6 +1070,128 @@ describe('sanitizeConnectorOutboundContent', () => {
|
|
|
1070
1070
|
assert.equal(output.mainThreadMessageCount, 0)
|
|
1071
1071
|
})
|
|
1072
1072
|
|
|
1073
|
+
it('does not suppress other senders just because their name appears inside the matched quiet-boundary memory', () => {
|
|
1074
|
+
const output = runWithTempDataDir(`
|
|
1075
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
1076
|
+
const managerMod = await import('./src/lib/server/connectors/manager')
|
|
1077
|
+
const providersMod = await import('./src/lib/providers/index')
|
|
1078
|
+
const pluginsMod = await import('./src/lib/server/plugins')
|
|
1079
|
+
const memoryDbMod = await import('./src/lib/server/memory/memory-db')
|
|
1080
|
+
const storage = storageMod.default || storageMod
|
|
1081
|
+
const manager = managerMod.default || managerMod
|
|
1082
|
+
const providers = providersMod.default || providersMod
|
|
1083
|
+
const plugins = pluginsMod.default || pluginsMod
|
|
1084
|
+
const memoryDbApi = memoryDbMod.default || memoryDbMod
|
|
1085
|
+
const memoryDb = memoryDbApi.getMemoryDb()
|
|
1086
|
+
|
|
1087
|
+
let providerCalls = 0
|
|
1088
|
+
const now = Date.now()
|
|
1089
|
+
providers.PROVIDERS['test-provider'] = {
|
|
1090
|
+
id: 'test-provider',
|
|
1091
|
+
name: 'Test Provider',
|
|
1092
|
+
models: ['test-model'],
|
|
1093
|
+
requiresApiKey: false,
|
|
1094
|
+
requiresEndpoint: false,
|
|
1095
|
+
handler: {
|
|
1096
|
+
streamChat: async (opts) => {
|
|
1097
|
+
providerCalls += 1
|
|
1098
|
+
opts.write('data: ' + JSON.stringify({ t: 'r', text: 'Replying to Riley normally' }) + '\\n')
|
|
1099
|
+
return ''
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
plugins.getPluginManager().registerBuiltin('test-quiet-boundary-connector-plugin-allow-riley', {
|
|
1105
|
+
name: 'Test Quiet Boundary Connector Plugin Allow Riley',
|
|
1106
|
+
connectors: [{
|
|
1107
|
+
id: 'test-quiet-allow-riley',
|
|
1108
|
+
name: 'Test Quiet Allow Riley',
|
|
1109
|
+
description: 'Quiet boundary false-positive test connector',
|
|
1110
|
+
startListener: async () => async () => {},
|
|
1111
|
+
sendMessage: async () => ({ messageId: 'unused' }),
|
|
1112
|
+
}],
|
|
1113
|
+
})
|
|
1114
|
+
|
|
1115
|
+
storage.saveSettings({})
|
|
1116
|
+
storage.saveAgents({
|
|
1117
|
+
agent_1: {
|
|
1118
|
+
id: 'agent_1',
|
|
1119
|
+
name: 'Nova',
|
|
1120
|
+
provider: 'test-provider',
|
|
1121
|
+
model: 'test-model',
|
|
1122
|
+
plugins: [],
|
|
1123
|
+
threadSessionId: 'agent_thread',
|
|
1124
|
+
createdAt: now,
|
|
1125
|
+
updatedAt: now,
|
|
1126
|
+
},
|
|
1127
|
+
})
|
|
1128
|
+
storage.saveConnectors({
|
|
1129
|
+
conn_1: {
|
|
1130
|
+
id: 'conn_1',
|
|
1131
|
+
name: 'WhatsApp',
|
|
1132
|
+
platform: 'test-quiet-allow-riley',
|
|
1133
|
+
agentId: 'agent_1',
|
|
1134
|
+
credentialId: null,
|
|
1135
|
+
config: { inboundDebounceMs: 0 },
|
|
1136
|
+
isEnabled: true,
|
|
1137
|
+
status: 'stopped',
|
|
1138
|
+
createdAt: now,
|
|
1139
|
+
updatedAt: now,
|
|
1140
|
+
},
|
|
1141
|
+
})
|
|
1142
|
+
storage.saveSessions({
|
|
1143
|
+
agent_thread: {
|
|
1144
|
+
id: 'agent_thread',
|
|
1145
|
+
name: 'Nova',
|
|
1146
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
1147
|
+
user: 'default',
|
|
1148
|
+
provider: 'test-provider',
|
|
1149
|
+
model: 'test-model',
|
|
1150
|
+
claudeSessionId: null,
|
|
1151
|
+
messages: [],
|
|
1152
|
+
createdAt: now,
|
|
1153
|
+
lastActiveAt: now,
|
|
1154
|
+
sessionType: 'human',
|
|
1155
|
+
agentId: 'agent_1',
|
|
1156
|
+
plugins: [],
|
|
1157
|
+
},
|
|
1158
|
+
})
|
|
1159
|
+
|
|
1160
|
+
memoryDb.add({
|
|
1161
|
+
agentId: 'agent_1',
|
|
1162
|
+
sessionId: null,
|
|
1163
|
+
category: 'identity/preferences',
|
|
1164
|
+
title: 'Wife communication rule',
|
|
1165
|
+
content: 'Riley\\'s partner (+44 7700 900111): Do NOT respond unless she addresses Nova directly or mentions Nova directly. If unsure, verify whether the message is meant for the agent before responding.',
|
|
1166
|
+
})
|
|
1167
|
+
|
|
1168
|
+
const connector = storage.loadConnectors().conn_1
|
|
1169
|
+
const response = await manager.routeConnectorMessageForTest(connector, {
|
|
1170
|
+
platform: 'whatsapp',
|
|
1171
|
+
channelId: '447700900123@s.whatsapp.net',
|
|
1172
|
+
senderId: '447700900123@s.whatsapp.net',
|
|
1173
|
+
senderName: 'Riley',
|
|
1174
|
+
text: 'Did you see the last update?',
|
|
1175
|
+
messageId: 'in-quiet-riley-1',
|
|
1176
|
+
isGroup: false,
|
|
1177
|
+
})
|
|
1178
|
+
|
|
1179
|
+
const sessions = storage.loadSessions()
|
|
1180
|
+
const directSession = Object.values(sessions).find((entry) => entry.id !== 'agent_thread')
|
|
1181
|
+
console.log(JSON.stringify({
|
|
1182
|
+
response,
|
|
1183
|
+
providerCalls,
|
|
1184
|
+
directSessionMessageCount: directSession?.messages?.length || 0,
|
|
1185
|
+
mainThreadMessageCount: sessions.agent_thread?.messages?.length || 0,
|
|
1186
|
+
}))
|
|
1187
|
+
`)
|
|
1188
|
+
|
|
1189
|
+
assert.equal(output.response, 'Replying to Riley normally')
|
|
1190
|
+
assert.equal(output.providerCalls, 1)
|
|
1191
|
+
assert.equal(output.directSessionMessageCount, 2)
|
|
1192
|
+
assert.equal(output.mainThreadMessageCount, 2)
|
|
1193
|
+
})
|
|
1194
|
+
|
|
1073
1195
|
it('requires an explicit target when a shared thread only has mirrored connector history', () => {
|
|
1074
1196
|
const output = runWithTempDataDir(`
|
|
1075
1197
|
const fs = await import('node:fs')
|
|
@@ -90,6 +90,269 @@ describe('plugin manager hook execution', () => {
|
|
|
90
90
|
assert.deepEqual(withEnable, { original: true, patched: true })
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
+
it('merges beforePromptBuild context and preserves first system prompt override', async () => {
|
|
94
|
+
const pluginA = uniquePluginId('before_prompt_build_a')
|
|
95
|
+
const pluginB = uniquePluginId('before_prompt_build_b')
|
|
96
|
+
const session = {
|
|
97
|
+
id: 'prompt-hook-session',
|
|
98
|
+
name: 'Prompt Hook Session',
|
|
99
|
+
cwd: process.cwd(),
|
|
100
|
+
user: 'tester',
|
|
101
|
+
provider: 'openai',
|
|
102
|
+
model: 'gpt-test',
|
|
103
|
+
claudeSessionId: null,
|
|
104
|
+
messages: [],
|
|
105
|
+
createdAt: Date.now(),
|
|
106
|
+
lastActiveAt: Date.now(),
|
|
107
|
+
plugins: [pluginA, pluginB],
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getPluginManager().registerBuiltin(pluginA, {
|
|
111
|
+
name: 'Before Prompt Build A',
|
|
112
|
+
hooks: {
|
|
113
|
+
beforePromptBuild: () => ({
|
|
114
|
+
systemPrompt: 'system A',
|
|
115
|
+
prependContext: 'context A',
|
|
116
|
+
prependSystemContext: 'prepend A',
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
getPluginManager().registerBuiltin(pluginB, {
|
|
121
|
+
name: 'Before Prompt Build B',
|
|
122
|
+
hooks: {
|
|
123
|
+
beforePromptBuild: () => ({
|
|
124
|
+
systemPrompt: 'system B',
|
|
125
|
+
prependContext: 'context B',
|
|
126
|
+
appendSystemContext: 'append B',
|
|
127
|
+
}),
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const result = await getPluginManager().runBeforePromptBuild(
|
|
132
|
+
{
|
|
133
|
+
session,
|
|
134
|
+
prompt: 'base prompt',
|
|
135
|
+
message: 'hello',
|
|
136
|
+
history: [],
|
|
137
|
+
messages: [],
|
|
138
|
+
},
|
|
139
|
+
{ enabledIds: [pluginA, pluginB] },
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
assert.deepEqual(result, {
|
|
143
|
+
systemPrompt: 'system A',
|
|
144
|
+
prependContext: 'context A\n\ncontext B',
|
|
145
|
+
prependSystemContext: 'prepend A',
|
|
146
|
+
appendSystemContext: 'append B',
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('applies beforeToolCall params merges and block results before legacy beforeToolExec', async () => {
|
|
151
|
+
const pluginA = uniquePluginId('before_tool_call_a')
|
|
152
|
+
const pluginB = uniquePluginId('before_tool_call_b')
|
|
153
|
+
const session = {
|
|
154
|
+
id: 'tool-hook-session',
|
|
155
|
+
name: 'Tool Hook Session',
|
|
156
|
+
cwd: process.cwd(),
|
|
157
|
+
user: 'tester',
|
|
158
|
+
provider: 'openai',
|
|
159
|
+
model: 'gpt-test',
|
|
160
|
+
claudeSessionId: null,
|
|
161
|
+
messages: [],
|
|
162
|
+
createdAt: Date.now(),
|
|
163
|
+
lastActiveAt: Date.now(),
|
|
164
|
+
plugins: [pluginA, pluginB],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getPluginManager().registerBuiltin(pluginA, {
|
|
168
|
+
name: 'Before Tool Call A',
|
|
169
|
+
hooks: {
|
|
170
|
+
beforeToolCall: () => ({
|
|
171
|
+
params: { patched: true },
|
|
172
|
+
warning: 'tool warning',
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
getPluginManager().registerBuiltin(pluginB, {
|
|
177
|
+
name: 'Before Tool Call B',
|
|
178
|
+
hooks: {
|
|
179
|
+
beforeToolCall: ({ input }) => ({
|
|
180
|
+
block: true,
|
|
181
|
+
blockReason: `blocked with patched=${String(input?.patched)}`,
|
|
182
|
+
}),
|
|
183
|
+
beforeToolExec: () => ({ shouldNotRun: true }),
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const result = await getPluginManager().runBeforeToolCall(
|
|
188
|
+
{
|
|
189
|
+
session,
|
|
190
|
+
toolName: 'shell',
|
|
191
|
+
input: { original: true },
|
|
192
|
+
runId: 'run-1',
|
|
193
|
+
},
|
|
194
|
+
{ enabledIds: [pluginA, pluginB] },
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
assert.deepEqual(result, {
|
|
198
|
+
input: { original: true, patched: true },
|
|
199
|
+
blockReason: 'blocked with patched=true',
|
|
200
|
+
warning: 'tool warning',
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('applies beforeModelResolve overrides in plugin order', async () => {
|
|
205
|
+
const pluginA = uniquePluginId('before_model_resolve_a')
|
|
206
|
+
const pluginB = uniquePluginId('before_model_resolve_b')
|
|
207
|
+
const session = {
|
|
208
|
+
id: 'model-resolve-session',
|
|
209
|
+
name: 'Model Resolve Session',
|
|
210
|
+
cwd: process.cwd(),
|
|
211
|
+
user: 'tester',
|
|
212
|
+
provider: 'openai',
|
|
213
|
+
model: 'gpt-test',
|
|
214
|
+
claudeSessionId: null,
|
|
215
|
+
messages: [],
|
|
216
|
+
createdAt: Date.now(),
|
|
217
|
+
lastActiveAt: Date.now(),
|
|
218
|
+
plugins: [pluginA, pluginB],
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getPluginManager().registerBuiltin(pluginA, {
|
|
222
|
+
name: 'Before Model Resolve A',
|
|
223
|
+
hooks: {
|
|
224
|
+
beforeModelResolve: () => ({
|
|
225
|
+
providerOverride: 'ollama',
|
|
226
|
+
modelOverride: 'llama-a',
|
|
227
|
+
}),
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
getPluginManager().registerBuiltin(pluginB, {
|
|
231
|
+
name: 'Before Model Resolve B',
|
|
232
|
+
hooks: {
|
|
233
|
+
beforeModelResolve: () => ({
|
|
234
|
+
modelOverride: 'llama-b',
|
|
235
|
+
apiEndpointOverride: 'http://127.0.0.1:11434',
|
|
236
|
+
}),
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const result = await getPluginManager().runBeforeModelResolve(
|
|
241
|
+
{
|
|
242
|
+
session,
|
|
243
|
+
prompt: 'base prompt',
|
|
244
|
+
message: 'hello',
|
|
245
|
+
provider: session.provider,
|
|
246
|
+
model: session.model,
|
|
247
|
+
apiEndpoint: null,
|
|
248
|
+
},
|
|
249
|
+
{ enabledIds: [pluginA, pluginB] },
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
assert.deepEqual(result, {
|
|
253
|
+
providerOverride: 'ollama',
|
|
254
|
+
modelOverride: 'llama-b',
|
|
255
|
+
apiEndpointOverride: 'http://127.0.0.1:11434',
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('chains toolResultPersist and beforeMessageWrite hooks', async () => {
|
|
260
|
+
const pluginA = uniquePluginId('tool_result_persist_a')
|
|
261
|
+
const pluginB = uniquePluginId('before_message_write_b')
|
|
262
|
+
const session = {
|
|
263
|
+
id: 'message-write-session',
|
|
264
|
+
name: 'Message Write Session',
|
|
265
|
+
cwd: process.cwd(),
|
|
266
|
+
user: 'tester',
|
|
267
|
+
provider: 'openai',
|
|
268
|
+
model: 'gpt-test',
|
|
269
|
+
claudeSessionId: null,
|
|
270
|
+
messages: [],
|
|
271
|
+
createdAt: Date.now(),
|
|
272
|
+
lastActiveAt: Date.now(),
|
|
273
|
+
plugins: [pluginA, pluginB],
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
getPluginManager().registerBuiltin(pluginA, {
|
|
277
|
+
name: 'Tool Result Persist A',
|
|
278
|
+
hooks: {
|
|
279
|
+
toolResultPersist: ({ message, toolName }) => ({
|
|
280
|
+
...message,
|
|
281
|
+
text: `${message.text} [tool:${toolName}]`,
|
|
282
|
+
}),
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
getPluginManager().registerBuiltin(pluginB, {
|
|
286
|
+
name: 'Before Message Write B',
|
|
287
|
+
hooks: {
|
|
288
|
+
beforeMessageWrite: ({ message }) => ({
|
|
289
|
+
message: {
|
|
290
|
+
...message,
|
|
291
|
+
text: `${message.text} [persisted]`,
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
},
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const persisted = await getPluginManager().runToolResultPersist(
|
|
298
|
+
{
|
|
299
|
+
session,
|
|
300
|
+
message: {
|
|
301
|
+
role: 'assistant',
|
|
302
|
+
text: 'tool output',
|
|
303
|
+
time: Date.now(),
|
|
304
|
+
},
|
|
305
|
+
toolName: 'shell',
|
|
306
|
+
toolCallId: 'call-1',
|
|
307
|
+
},
|
|
308
|
+
{ enabledIds: [pluginA, pluginB] },
|
|
309
|
+
)
|
|
310
|
+
const writeResult = await getPluginManager().runBeforeMessageWrite(
|
|
311
|
+
{
|
|
312
|
+
session,
|
|
313
|
+
message: persisted,
|
|
314
|
+
phase: 'assistant_final',
|
|
315
|
+
runId: 'run-1',
|
|
316
|
+
},
|
|
317
|
+
{ enabledIds: [pluginA, pluginB] },
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
assert.equal(writeResult.block, false)
|
|
321
|
+
assert.equal(writeResult.message.text, 'tool output [tool:shell] [persisted]')
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('blocks subagent spawning when a plugin hook rejects it', async () => {
|
|
325
|
+
const pluginId = uniquePluginId('subagent_spawning')
|
|
326
|
+
|
|
327
|
+
getPluginManager().registerBuiltin(pluginId, {
|
|
328
|
+
name: 'Subagent Spawning Hook',
|
|
329
|
+
hooks: {
|
|
330
|
+
subagentSpawning: () => ({
|
|
331
|
+
status: 'error',
|
|
332
|
+
error: 'blocked by lifecycle hook',
|
|
333
|
+
}),
|
|
334
|
+
},
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
const result = await getPluginManager().runSubagentSpawning(
|
|
338
|
+
{
|
|
339
|
+
parentSessionId: 'parent-1',
|
|
340
|
+
agentId: 'agent-1',
|
|
341
|
+
agentName: 'Agent One',
|
|
342
|
+
message: 'do the work',
|
|
343
|
+
cwd: process.cwd(),
|
|
344
|
+
mode: 'run',
|
|
345
|
+
threadRequested: false,
|
|
346
|
+
},
|
|
347
|
+
{ enabledIds: [pluginId] },
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
assert.deepEqual(result, {
|
|
351
|
+
status: 'error',
|
|
352
|
+
error: 'blocked by lifecycle hook',
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
93
356
|
it('chains text transforms in plugin order', async () => {
|
|
94
357
|
const pluginA = uniquePluginId('transform_a')
|
|
95
358
|
const pluginB = uniquePluginId('transform_b')
|