@actiondock/testing 2.0.8 → 2.0.10
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/package.json +10 -4
- package/src/runtime.ts +1 -3
- package/src/storage.ts +9 -573
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actiondock/testing",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
4
4
|
"description": "ActionDock Test Runtime for testing Actions with real Core execution semantics",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -27,14 +27,20 @@
|
|
|
27
27
|
"test": "bun test"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@actiondock/core": "^2.0.
|
|
31
|
-
"@actiondock/sdk": "^2.0.
|
|
30
|
+
"@actiondock/core": "^2.0.10",
|
|
31
|
+
"@actiondock/sdk": "^2.0.10"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/bun": "latest",
|
|
35
35
|
"typescript": "^5.7.0"
|
|
36
36
|
},
|
|
37
|
-
"keywords": [
|
|
37
|
+
"keywords": [
|
|
38
|
+
"actiondock",
|
|
39
|
+
"testing",
|
|
40
|
+
"agent",
|
|
41
|
+
"actions",
|
|
42
|
+
"mock"
|
|
43
|
+
],
|
|
38
44
|
"author": "team4u",
|
|
39
45
|
"license": "Apache-2.0",
|
|
40
46
|
"repository": {
|
package/src/runtime.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
RuntimeStateStore,
|
|
8
8
|
setDefaultEventSink,
|
|
9
9
|
setProcessExecutor,
|
|
10
|
-
setSystemClock,
|
|
11
10
|
} from "@actiondock/core";
|
|
12
11
|
import type {
|
|
13
12
|
ActionDefinition,
|
|
@@ -226,8 +225,7 @@ export function createTestRuntime(options: TestRuntimeOptions = {}): TestRuntime
|
|
|
226
225
|
clock,
|
|
227
226
|
});
|
|
228
227
|
|
|
229
|
-
//
|
|
230
|
-
setSystemClock(clock);
|
|
228
|
+
// 全局注入测试进程执行器
|
|
231
229
|
setProcessExecutor(process);
|
|
232
230
|
|
|
233
231
|
// 初始化配置数据
|
package/src/storage.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Clock,
|
|
3
|
-
createDefaultSqliteDriver,
|
|
4
|
-
getSystemClock,
|
|
5
3
|
type RuntimeStorage,
|
|
4
|
+
SqliteRuntimeStorage,
|
|
6
5
|
type SqliteDriver,
|
|
7
6
|
type StateEntry,
|
|
8
7
|
type TerminalRunStatus,
|
|
@@ -23,579 +22,16 @@ export interface MemoryStorageOptions {
|
|
|
23
22
|
|
|
24
23
|
/**
|
|
25
24
|
* 统一内存运行时存储实现。
|
|
26
|
-
* 基于
|
|
25
|
+
* 基于 SqliteRuntimeStorage 构建,默认使用 :memory: 内存数据库并对接虚拟时钟,
|
|
26
|
+
* 确保与生产环境具备完全相同的配置优先级、状态过期契约与运行终态行为。
|
|
27
27
|
*/
|
|
28
|
-
export class MemoryStorage
|
|
29
|
-
private driver: SqliteDriver;
|
|
30
|
-
private packageId: string;
|
|
31
|
-
private clock: Clock;
|
|
32
|
-
private isClosed = false;
|
|
33
|
-
|
|
28
|
+
export class MemoryStorage extends SqliteRuntimeStorage {
|
|
34
29
|
constructor(options: MemoryStorageOptions = {}) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 初始化内存表结构与索引。
|
|
43
|
-
*/
|
|
44
|
-
private init(): void {
|
|
45
|
-
this.driver.exec("PRAGMA journal_mode = MEMORY;");
|
|
46
|
-
this.driver.exec("PRAGMA synchronous = OFF;");
|
|
47
|
-
|
|
48
|
-
this.driver.transaction(() => {
|
|
49
|
-
this.driver.exec(`
|
|
50
|
-
CREATE TABLE IF NOT EXISTS config (
|
|
51
|
-
package_id TEXT NOT NULL,
|
|
52
|
-
key TEXT NOT NULL,
|
|
53
|
-
value_json TEXT,
|
|
54
|
-
updated_at TEXT NOT NULL,
|
|
55
|
-
PRIMARY KEY (package_id, key)
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
CREATE TABLE IF NOT EXISTS state (
|
|
59
|
-
package_id TEXT NOT NULL,
|
|
60
|
-
namespace TEXT NOT NULL,
|
|
61
|
-
key TEXT NOT NULL,
|
|
62
|
-
value_json TEXT,
|
|
63
|
-
updated_at TEXT NOT NULL,
|
|
64
|
-
expires_at TEXT,
|
|
65
|
-
PRIMARY KEY (package_id, namespace, key)
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
CREATE TABLE IF NOT EXISTS runs (
|
|
69
|
-
id TEXT PRIMARY KEY,
|
|
70
|
-
root_run_id TEXT NOT NULL,
|
|
71
|
-
parent_run_id TEXT,
|
|
72
|
-
package_id TEXT NOT NULL,
|
|
73
|
-
package_instance_id TEXT NOT NULL,
|
|
74
|
-
action_id TEXT NOT NULL,
|
|
75
|
-
generation_id TEXT NOT NULL,
|
|
76
|
-
owner_id TEXT NOT NULL,
|
|
77
|
-
status TEXT NOT NULL,
|
|
78
|
-
input_json TEXT,
|
|
79
|
-
output_json TEXT,
|
|
80
|
-
error_json TEXT,
|
|
81
|
-
started_at TEXT NOT NULL,
|
|
82
|
-
finished_at TEXT,
|
|
83
|
-
duration_ms INTEGER
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
CREATE INDEX IF NOT EXISTS idx_runs_action ON runs(package_id, action_id);
|
|
87
|
-
CREATE INDEX IF NOT EXISTS idx_runs_root ON runs(root_run_id);
|
|
88
|
-
CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at DESC);
|
|
89
|
-
CREATE INDEX IF NOT EXISTS idx_state_expires ON state(expires_at);
|
|
90
|
-
`);
|
|
30
|
+
super({
|
|
31
|
+
packageId: options.packageId || "test-pkg",
|
|
32
|
+
dbPath: ":memory:",
|
|
33
|
+
driver: options.driver,
|
|
34
|
+
clock: options.clock,
|
|
91
35
|
});
|
|
92
36
|
}
|
|
93
|
-
|
|
94
|
-
// --- 配置管理 ---
|
|
95
|
-
|
|
96
|
-
getConfig<T = unknown>(key: string): T | undefined {
|
|
97
|
-
const stmt = this.driver.prepare(
|
|
98
|
-
"SELECT value_json FROM config WHERE package_id = ? AND key = ?"
|
|
99
|
-
);
|
|
100
|
-
const row = stmt.get<{ value_json: string }>(this.packageId, key);
|
|
101
|
-
if (!row || row.value_json === undefined || row.value_json === null) {
|
|
102
|
-
return undefined;
|
|
103
|
-
}
|
|
104
|
-
try {
|
|
105
|
-
return JSON.parse(row.value_json) as T;
|
|
106
|
-
} catch {
|
|
107
|
-
return row.value_json as unknown as T;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
listConfig(): Record<string, unknown> {
|
|
112
|
-
const stmt = this.driver.prepare(
|
|
113
|
-
"SELECT key, value_json FROM config WHERE package_id = ?"
|
|
114
|
-
);
|
|
115
|
-
const rows = stmt.all<{ key: string; value_json: string }>(this.packageId);
|
|
116
|
-
const result: Record<string, unknown> = {};
|
|
117
|
-
for (const row of rows) {
|
|
118
|
-
try {
|
|
119
|
-
result[row.key] = JSON.parse(row.value_json);
|
|
120
|
-
} catch {
|
|
121
|
-
result[row.key] = row.value_json;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return result;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
setConfig(key: string, value: unknown): void {
|
|
128
|
-
const stmt = this.driver.prepare(`
|
|
129
|
-
INSERT INTO config (package_id, key, value_json, updated_at)
|
|
130
|
-
VALUES (?, ?, ?, ?)
|
|
131
|
-
ON CONFLICT(package_id, key) DO UPDATE SET
|
|
132
|
-
value_json = excluded.value_json,
|
|
133
|
-
updated_at = excluded.updated_at
|
|
134
|
-
`);
|
|
135
|
-
const valJson = JSON.stringify(value);
|
|
136
|
-
const now = this.clock.now().toISOString();
|
|
137
|
-
stmt.run(this.packageId, key, valJson, now);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
deleteConfig(key: string): boolean {
|
|
141
|
-
const stmt = this.driver.prepare(
|
|
142
|
-
"DELETE FROM config WHERE package_id = ? AND key = ?"
|
|
143
|
-
);
|
|
144
|
-
const res = stmt.run(this.packageId, key);
|
|
145
|
-
return res.changes > 0;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// --- 状态管理 ---
|
|
149
|
-
|
|
150
|
-
async getState<T = unknown>(
|
|
151
|
-
namespace: string,
|
|
152
|
-
key: string
|
|
153
|
-
): Promise<T | undefined> {
|
|
154
|
-
const stmt = this.driver.prepare(
|
|
155
|
-
"SELECT value_json, expires_at FROM state WHERE package_id = ? AND namespace = ? AND key = ?"
|
|
156
|
-
);
|
|
157
|
-
const row = stmt.get<{ value_json: string; expires_at?: string }>(
|
|
158
|
-
this.packageId,
|
|
159
|
-
namespace,
|
|
160
|
-
key
|
|
161
|
-
);
|
|
162
|
-
if (!row || row.value_json === undefined || row.value_json === null) {
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (row.expires_at) {
|
|
167
|
-
const expires = new Date(row.expires_at).getTime();
|
|
168
|
-
const current = this.clock.now().getTime();
|
|
169
|
-
if (current >= expires) {
|
|
170
|
-
this.deleteState(namespace, key).catch(() => {});
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
return JSON.parse(row.value_json) as T;
|
|
177
|
-
} catch {
|
|
178
|
-
return row.value_json as unknown as T;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async findState<T = unknown>(
|
|
183
|
-
targetKey: string,
|
|
184
|
-
namespace?: string
|
|
185
|
-
): Promise<StateEntry | undefined> {
|
|
186
|
-
let ns = namespace;
|
|
187
|
-
let actualKey = targetKey;
|
|
188
|
-
|
|
189
|
-
if (!ns && targetKey.includes(":")) {
|
|
190
|
-
const parts = targetKey.split(":");
|
|
191
|
-
ns = parts[0];
|
|
192
|
-
actualKey = parts.slice(1).join(":");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (ns) {
|
|
196
|
-
const val = await this.getState<T>(ns, actualKey);
|
|
197
|
-
if (val === undefined) return undefined;
|
|
198
|
-
|
|
199
|
-
const stmt = this.driver.prepare(
|
|
200
|
-
"SELECT updated_at, expires_at FROM state WHERE package_id = ? AND namespace = ? AND key = ?"
|
|
201
|
-
);
|
|
202
|
-
const row = stmt.get<{ updated_at: string; expires_at?: string }>(
|
|
203
|
-
this.packageId,
|
|
204
|
-
ns,
|
|
205
|
-
actualKey
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
packageId: this.packageId,
|
|
210
|
-
namespace: ns,
|
|
211
|
-
key: actualKey,
|
|
212
|
-
fullKey: targetKey,
|
|
213
|
-
value: val,
|
|
214
|
-
updatedAt: row?.updated_at || this.clock.now().toISOString(),
|
|
215
|
-
expiresAt: row?.expires_at,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const val = await this.getState<T>("", actualKey);
|
|
220
|
-
if (val !== undefined) {
|
|
221
|
-
const stmt = this.driver.prepare(
|
|
222
|
-
"SELECT updated_at, expires_at FROM state WHERE package_id = ? AND namespace = ? AND key = ?"
|
|
223
|
-
);
|
|
224
|
-
const row = stmt.get<{ updated_at: string; expires_at?: string }>(
|
|
225
|
-
this.packageId,
|
|
226
|
-
"",
|
|
227
|
-
actualKey
|
|
228
|
-
);
|
|
229
|
-
return {
|
|
230
|
-
packageId: this.packageId,
|
|
231
|
-
namespace: "",
|
|
232
|
-
key: actualKey,
|
|
233
|
-
fullKey: actualKey,
|
|
234
|
-
value: val,
|
|
235
|
-
updatedAt: row?.updated_at || this.clock.now().toISOString(),
|
|
236
|
-
expiresAt: row?.expires_at,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const stmt = this.driver.prepare(
|
|
241
|
-
"SELECT namespace, key, value_json, updated_at, expires_at FROM state WHERE package_id = ? AND key = ?"
|
|
242
|
-
);
|
|
243
|
-
const rows = stmt.all<{
|
|
244
|
-
namespace: string;
|
|
245
|
-
key: string;
|
|
246
|
-
value_json: string;
|
|
247
|
-
updated_at: string;
|
|
248
|
-
expires_at?: string;
|
|
249
|
-
}>(this.packageId, actualKey);
|
|
250
|
-
|
|
251
|
-
const now = this.clock.now().getTime();
|
|
252
|
-
for (const row of rows) {
|
|
253
|
-
if (row.expires_at && now >= new Date(row.expires_at).getTime()) {
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
let parsedVal: unknown;
|
|
257
|
-
try {
|
|
258
|
-
parsedVal = JSON.parse(row.value_json);
|
|
259
|
-
} catch {
|
|
260
|
-
parsedVal = row.value_json;
|
|
261
|
-
}
|
|
262
|
-
return {
|
|
263
|
-
packageId: this.packageId,
|
|
264
|
-
namespace: row.namespace,
|
|
265
|
-
key: row.key,
|
|
266
|
-
fullKey: row.namespace ? `${row.namespace}:${row.key}` : row.key,
|
|
267
|
-
value: parsedVal,
|
|
268
|
-
updatedAt: row.updated_at,
|
|
269
|
-
expiresAt: row.expires_at,
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return undefined;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async setState<T = unknown>(
|
|
277
|
-
namespace: string,
|
|
278
|
-
key: string,
|
|
279
|
-
value: T,
|
|
280
|
-
ttl?: number
|
|
281
|
-
): Promise<void> {
|
|
282
|
-
const stmt = this.driver.prepare(`
|
|
283
|
-
INSERT INTO state (package_id, namespace, key, value_json, updated_at, expires_at)
|
|
284
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
285
|
-
ON CONFLICT(package_id, namespace, key) DO UPDATE SET
|
|
286
|
-
value_json = excluded.value_json,
|
|
287
|
-
updated_at = excluded.updated_at,
|
|
288
|
-
expires_at = excluded.expires_at
|
|
289
|
-
`);
|
|
290
|
-
const valJson = JSON.stringify(value);
|
|
291
|
-
const now = this.clock.now();
|
|
292
|
-
const updatedAt = now.toISOString();
|
|
293
|
-
|
|
294
|
-
let expiresAt: string | null = null;
|
|
295
|
-
if (typeof ttl === "number" && ttl > 0) {
|
|
296
|
-
expiresAt = new Date(now.getTime() + ttl * 1000).toISOString();
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
stmt.run(this.packageId, namespace, key, valJson, updatedAt, expiresAt);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async deleteState(namespace: string, key: string): Promise<boolean> {
|
|
303
|
-
const stmt = this.driver.prepare(
|
|
304
|
-
"DELETE FROM state WHERE package_id = ? AND namespace = ? AND key = ?"
|
|
305
|
-
);
|
|
306
|
-
const res = stmt.run(this.packageId, namespace, key);
|
|
307
|
-
return res.changes > 0;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async deleteStateSmart(
|
|
311
|
-
targetKey: string,
|
|
312
|
-
namespace?: string
|
|
313
|
-
): Promise<boolean> {
|
|
314
|
-
let ns = namespace;
|
|
315
|
-
let actualKey = targetKey;
|
|
316
|
-
|
|
317
|
-
if (!ns && targetKey.includes(":")) {
|
|
318
|
-
const parts = targetKey.split(":");
|
|
319
|
-
ns = parts[0];
|
|
320
|
-
actualKey = parts.slice(1).join(":");
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (ns !== undefined) {
|
|
324
|
-
return this.deleteState(ns, actualKey);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const deletedRoot = await this.deleteState("", actualKey);
|
|
328
|
-
if (deletedRoot) return true;
|
|
329
|
-
|
|
330
|
-
const stmt = this.driver.prepare(
|
|
331
|
-
"DELETE FROM state WHERE package_id = ? AND key = ?"
|
|
332
|
-
);
|
|
333
|
-
const res = stmt.run(this.packageId, actualKey);
|
|
334
|
-
return res.changes > 0;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
async clearState(
|
|
338
|
-
options: { namespace?: string; all?: boolean; prefix?: string } = {}
|
|
339
|
-
): Promise<number> {
|
|
340
|
-
let sql = "DELETE FROM state WHERE package_id = ?";
|
|
341
|
-
const params: unknown[] = [this.packageId];
|
|
342
|
-
|
|
343
|
-
if (options.namespace !== undefined) {
|
|
344
|
-
sql += " AND namespace = ?";
|
|
345
|
-
params.push(options.namespace);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (options.prefix) {
|
|
349
|
-
sql += " AND key LIKE ? ESCAPE '\\'";
|
|
350
|
-
const escapedPrefix = options.prefix.replace(/([%_\\])/g, "\\$1");
|
|
351
|
-
params.push(`${escapedPrefix}%`);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const stmt = this.driver.prepare(sql);
|
|
355
|
-
const res = stmt.run(...params);
|
|
356
|
-
return res.changes;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
async listStateKeys(
|
|
360
|
-
namespace?: string | null,
|
|
361
|
-
prefix?: string
|
|
362
|
-
): Promise<string[]> {
|
|
363
|
-
let sql = "SELECT namespace, key, expires_at FROM state WHERE package_id = ?";
|
|
364
|
-
const params: unknown[] = [this.packageId];
|
|
365
|
-
|
|
366
|
-
if (namespace !== null && namespace !== undefined) {
|
|
367
|
-
sql += " AND namespace = ?";
|
|
368
|
-
params.push(namespace);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (prefix) {
|
|
372
|
-
sql += " AND key LIKE ? ESCAPE '\\'";
|
|
373
|
-
const escapedPrefix = prefix.replace(/([%_\\])/g, "\\$1");
|
|
374
|
-
params.push(`${escapedPrefix}%`);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const stmt = this.driver.prepare(sql);
|
|
378
|
-
const rows = stmt.all<{
|
|
379
|
-
namespace: string;
|
|
380
|
-
key: string;
|
|
381
|
-
expires_at?: string;
|
|
382
|
-
}>(...params);
|
|
383
|
-
|
|
384
|
-
const now = this.clock.now().getTime();
|
|
385
|
-
const result: string[] = [];
|
|
386
|
-
|
|
387
|
-
for (const row of rows) {
|
|
388
|
-
if (row.expires_at && now >= new Date(row.expires_at).getTime()) {
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
if (namespace !== null && namespace !== undefined) {
|
|
392
|
-
result.push(row.key);
|
|
393
|
-
} else {
|
|
394
|
-
result.push(row.namespace ? `${row.namespace}:${row.key}` : row.key);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return result;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
async listStateEntries(
|
|
402
|
-
options: { namespace?: string; prefix?: string } = {}
|
|
403
|
-
): Promise<StateEntry[]> {
|
|
404
|
-
let sql = "SELECT namespace, key, value_json, updated_at, expires_at FROM state WHERE package_id = ?";
|
|
405
|
-
const params: unknown[] = [this.packageId];
|
|
406
|
-
|
|
407
|
-
if (options.namespace !== undefined) {
|
|
408
|
-
sql += " AND namespace = ?";
|
|
409
|
-
params.push(options.namespace);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (options.prefix) {
|
|
413
|
-
sql += " AND key LIKE ? ESCAPE '\\'";
|
|
414
|
-
const escapedPrefix = options.prefix.replace(/([%_\\])/g, "\\$1");
|
|
415
|
-
params.push(`${escapedPrefix}%`);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const stmt = this.driver.prepare(sql);
|
|
419
|
-
const rows = stmt.all<{
|
|
420
|
-
namespace: string;
|
|
421
|
-
key: string;
|
|
422
|
-
value_json: string;
|
|
423
|
-
updated_at: string;
|
|
424
|
-
expires_at?: string;
|
|
425
|
-
}>(...params);
|
|
426
|
-
|
|
427
|
-
const now = this.clock.now().getTime();
|
|
428
|
-
const results: StateEntry[] = [];
|
|
429
|
-
|
|
430
|
-
for (const row of rows) {
|
|
431
|
-
if (row.expires_at && now >= new Date(row.expires_at).getTime()) {
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
let parsedVal: unknown;
|
|
436
|
-
try {
|
|
437
|
-
parsedVal = JSON.parse(row.value_json);
|
|
438
|
-
} catch {
|
|
439
|
-
parsedVal = row.value_json;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
results.push({
|
|
443
|
-
packageId: this.packageId,
|
|
444
|
-
namespace: row.namespace,
|
|
445
|
-
key: row.key,
|
|
446
|
-
fullKey: row.namespace ? `${row.namespace}:${row.key}` : row.key,
|
|
447
|
-
value: parsedVal,
|
|
448
|
-
updatedAt: row.updated_at,
|
|
449
|
-
expiresAt: row.expires_at,
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return results;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// --- 运行记录管理 ---
|
|
457
|
-
|
|
458
|
-
createRun(record: RunRecord): void {
|
|
459
|
-
const stmt = this.driver.prepare(`
|
|
460
|
-
INSERT INTO runs (
|
|
461
|
-
id, root_run_id, parent_run_id, package_id, package_instance_id,
|
|
462
|
-
action_id, generation_id, owner_id, status, input_json, output_json,
|
|
463
|
-
error_json, started_at, finished_at, duration_ms
|
|
464
|
-
)
|
|
465
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
466
|
-
`);
|
|
467
|
-
|
|
468
|
-
const rootRunId = record.rootRunId || record.id;
|
|
469
|
-
const parentRunId = record.parentRunId || null;
|
|
470
|
-
const packageInstanceId = record.packageInstanceId || record.packageId || this.packageId;
|
|
471
|
-
const generationId = record.generationId || "1";
|
|
472
|
-
const ownerId = record.ownerId || "local";
|
|
473
|
-
const inputJson = record.input !== undefined ? JSON.stringify(record.input) : null;
|
|
474
|
-
const outputJson = record.output !== undefined ? JSON.stringify(record.output) : null;
|
|
475
|
-
const errorJson = record.error ? JSON.stringify(record.error) : null;
|
|
476
|
-
const finishedAt = record.finishedAt || null;
|
|
477
|
-
const durationMs = typeof record.durationMs === "number" ? record.durationMs : null;
|
|
478
|
-
|
|
479
|
-
stmt.run(
|
|
480
|
-
record.id,
|
|
481
|
-
rootRunId,
|
|
482
|
-
parentRunId,
|
|
483
|
-
record.packageId || this.packageId,
|
|
484
|
-
packageInstanceId,
|
|
485
|
-
record.actionId,
|
|
486
|
-
generationId,
|
|
487
|
-
ownerId,
|
|
488
|
-
record.status,
|
|
489
|
-
inputJson,
|
|
490
|
-
outputJson,
|
|
491
|
-
errorJson,
|
|
492
|
-
record.startedAt,
|
|
493
|
-
finishedAt,
|
|
494
|
-
durationMs
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
updateRun(
|
|
499
|
-
id: string,
|
|
500
|
-
status: TerminalRunStatus,
|
|
501
|
-
output?: unknown,
|
|
502
|
-
error?: RuntimeError,
|
|
503
|
-
finishedAt?: string
|
|
504
|
-
): void {
|
|
505
|
-
if (this.isClosed) return;
|
|
506
|
-
try {
|
|
507
|
-
const stmt = this.driver.prepare(`
|
|
508
|
-
UPDATE runs
|
|
509
|
-
SET status = ?, output_json = ?, error_json = ?, finished_at = ?
|
|
510
|
-
WHERE id = ?
|
|
511
|
-
`);
|
|
512
|
-
stmt.run(
|
|
513
|
-
status,
|
|
514
|
-
output !== undefined ? JSON.stringify(output) : null,
|
|
515
|
-
error ? JSON.stringify(error) : null,
|
|
516
|
-
finishedAt || this.clock.now().toISOString(),
|
|
517
|
-
id
|
|
518
|
-
);
|
|
519
|
-
} catch {
|
|
520
|
-
// 存储连接已释放时忽略
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
getRun(id: string): RunRecord | null {
|
|
525
|
-
const stmt = this.driver.prepare(
|
|
526
|
-
"SELECT * FROM runs WHERE id = ? AND package_id = ?"
|
|
527
|
-
);
|
|
528
|
-
const row = stmt.get<Record<string, unknown>>(id, this.packageId);
|
|
529
|
-
if (!row) return null;
|
|
530
|
-
return this.mapRunRecord(row);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
listRuns(options: { actionId?: string; limit?: number } = {}): RunRecord[] {
|
|
534
|
-
const limit = options.limit || 50;
|
|
535
|
-
let rows: Record<string, unknown>[];
|
|
536
|
-
if (options.actionId) {
|
|
537
|
-
const stmt = this.driver.prepare(`
|
|
538
|
-
SELECT * FROM runs
|
|
539
|
-
WHERE package_id = ? AND action_id = ?
|
|
540
|
-
ORDER BY started_at DESC
|
|
541
|
-
LIMIT ?
|
|
542
|
-
`);
|
|
543
|
-
rows = stmt.all<Record<string, unknown>>(this.packageId, options.actionId, limit);
|
|
544
|
-
} else {
|
|
545
|
-
const stmt = this.driver.prepare(`
|
|
546
|
-
SELECT * FROM runs
|
|
547
|
-
WHERE package_id = ?
|
|
548
|
-
ORDER BY started_at DESC
|
|
549
|
-
LIMIT ?
|
|
550
|
-
`);
|
|
551
|
-
rows = stmt.all<Record<string, unknown>>(this.packageId, limit);
|
|
552
|
-
}
|
|
553
|
-
return rows.map((r) => this.mapRunRecord(r));
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
clearRuns(options: { actionId?: string; status?: string } = {}): number {
|
|
557
|
-
let sql = "DELETE FROM runs WHERE package_id = ?";
|
|
558
|
-
const params: unknown[] = [this.packageId];
|
|
559
|
-
|
|
560
|
-
if (options.actionId) {
|
|
561
|
-
sql += " AND action_id = ?";
|
|
562
|
-
params.push(options.actionId);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (options.status) {
|
|
566
|
-
sql += " AND status = ?";
|
|
567
|
-
params.push(options.status);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const stmt = this.driver.prepare(sql);
|
|
571
|
-
const res = stmt.run(...params);
|
|
572
|
-
return res.changes;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
close(): void {
|
|
576
|
-
if (!this.isClosed) {
|
|
577
|
-
this.isClosed = true;
|
|
578
|
-
this.driver.close();
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
private mapRunRecord(row: Record<string, unknown>): RunRecord {
|
|
583
|
-
return {
|
|
584
|
-
id: String(row.id),
|
|
585
|
-
rootRunId: String(row.root_run_id || row.id),
|
|
586
|
-
parentRunId: row.parent_run_id ? String(row.parent_run_id) : undefined,
|
|
587
|
-
packageId: String(row.package_id),
|
|
588
|
-
packageInstanceId: String(row.package_instance_id || row.package_id),
|
|
589
|
-
actionId: String(row.action_id),
|
|
590
|
-
generationId: String(row.generation_id || "1"),
|
|
591
|
-
ownerId: String(row.owner_id || "local"),
|
|
592
|
-
status: row.status as RunRecord["status"],
|
|
593
|
-
input: row.input_json ? JSON.parse(String(row.input_json)) : undefined,
|
|
594
|
-
output: row.output_json ? JSON.parse(String(row.output_json)) : undefined,
|
|
595
|
-
error: row.error_json ? (JSON.parse(String(row.error_json)) as RuntimeError) : undefined,
|
|
596
|
-
startedAt: String(row.started_at),
|
|
597
|
-
finishedAt: row.finished_at ? String(row.finished_at) : undefined,
|
|
598
|
-
durationMs: typeof row.duration_ms === "number" ? row.duration_ms : undefined,
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
37
|
}
|