@actiondock/testing 2.2.1 → 2.3.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/runtime.d.ts CHANGED
@@ -40,6 +40,10 @@ import { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegmen
40
40
  export { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegment };
41
41
  /**
42
42
  * 基于内存 Map 的状态存储实现,支持命名空间隔离与 TTL 自动失效,专供单元测试使用。
43
+ *
44
+ * 根命名空间读取未命中时,会按 core 生产 RuntimeStateStore 的回扫语义,
45
+ * 以裸 key 反查全部命名空间中的同 key 条目(多条命中时抛歧义异常,与 core 的
46
+ * findState 契约一致),确保测试与生产行为不分叉。
43
47
  */
44
48
  export declare class MemoryStateStore implements StateStore {
45
49
  private store;
@@ -50,6 +54,13 @@ export declare class MemoryStateStore implements StateStore {
50
54
  private nowMs;
51
55
  private qualify;
52
56
  private extractEntry;
57
+ /** 提取并结算条目:命中且未过期返回条目本身,已过期删除并返回 undefined */
58
+ private settleEntry;
59
+ /**
60
+ * 以裸 key 回扫全部命名空间,对齐 core 生产存储的 findState 契约:
61
+ * 多条命中抛歧义异常,零命中返回 undefined,唯一命中返回该条目。
62
+ */
63
+ private findAcrossNamespaces;
53
64
  get<T = unknown>(key: string): Promise<T | undefined>;
54
65
  set<T = unknown>(key: string, value: T, ttl?: number): Promise<void>;
55
66
  delete(key: string): Promise<boolean>;
@@ -135,8 +146,12 @@ export interface TestRuntimeOptions {
135
146
  clock?: FakeClock;
136
147
  /** 可选注入的模拟进程执行器 */
137
148
  process?: MockProcessExecutor;
138
- /** 可选注入的日志记录器(默认使用 MemoryLogger) */
139
- logger?: Logger;
149
+ /**
150
+ * 可选注入的日志记录器,仅接受 MemoryLogger 实例(默认新建)。
151
+ * 测试运行时需捕获日志供断言检索,不支持自定义 Logger 实现;
152
+ * 如需自定义日志行为,请直接使用 DefaultExecutionService 组装。
153
+ */
154
+ logger?: MemoryLogger;
140
155
  /** 可选注入的底层存储实例 */
141
156
  storage?: MemoryStorage;
142
157
  /** 项目静态配置元数据 */
package/dist/runtime.js CHANGED
@@ -46,6 +46,10 @@ import { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegmen
46
46
  export { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegment };
47
47
  /**
48
48
  * 基于内存 Map 的状态存储实现,支持命名空间隔离与 TTL 自动失效,专供单元测试使用。
49
+ *
50
+ * 根命名空间读取未命中时,会按 core 生产 RuntimeStateStore 的回扫语义,
51
+ * 以裸 key 反查全部命名空间中的同 key 条目(多条命中时抛歧义异常,与 core 的
52
+ * findState 契约一致),确保测试与生产行为不分叉。
49
53
  */
50
54
  export class MemoryStateStore {
51
55
  store;
@@ -72,17 +76,67 @@ export class MemoryStateStore {
72
76
  }
73
77
  return { value: raw };
74
78
  }
75
- async get(key) {
76
- const qKey = this.qualify(key);
77
- const raw = this.store.get(qKey);
78
- if (raw === undefined)
79
- return undefined;
79
+ /** 提取并结算条目:命中且未过期返回条目本身,已过期删除并返回 undefined */
80
+ settleEntry(raw) {
80
81
  const entry = this.extractEntry(raw);
81
82
  if (entry.expiresAt !== undefined && entry.expiresAt <= this.nowMs()) {
82
- this.store.delete(qKey);
83
83
  return undefined;
84
84
  }
85
- return (entry.value !== undefined ? structuredClone(entry.value) : undefined);
85
+ return entry;
86
+ }
87
+ /**
88
+ * 以裸 key 回扫全部命名空间,对齐 core 生产存储的 findState 契约:
89
+ * 多条命中抛歧义异常,零命中返回 undefined,唯一命中返回该条目。
90
+ */
91
+ findAcrossNamespaces(key) {
92
+ const now = this.nowMs();
93
+ const hits = [];
94
+ for (const [storeKey, raw] of this.store.entries()) {
95
+ let decoded;
96
+ try {
97
+ decoded = decodeStateKey(storeKey);
98
+ }
99
+ catch {
100
+ // 无法解码的复合键直接跳过,不影响其他条目回扫
101
+ continue;
102
+ }
103
+ if (decoded.key !== key) {
104
+ continue;
105
+ }
106
+ const entry = this.extractEntry(raw);
107
+ if (entry.expiresAt !== undefined && entry.expiresAt <= now) {
108
+ this.store.delete(storeKey);
109
+ continue;
110
+ }
111
+ hits.push({ storeKey, entry });
112
+ }
113
+ if (hits.length > 1) {
114
+ throw new Error(`Ambiguous state key '${key}': matches ${hits.length} entries (${hits
115
+ .map((h) => decodeStateKey(h.storeKey).namespace + ":" + key)
116
+ .join(", ")})`);
117
+ }
118
+ if (hits.length === 0) {
119
+ return undefined;
120
+ }
121
+ const value = hits[0].entry.value;
122
+ return (value !== undefined ? structuredClone(value) : undefined);
123
+ }
124
+ async get(key) {
125
+ const qKey = this.qualify(key);
126
+ const raw = this.store.get(qKey);
127
+ if (raw !== undefined) {
128
+ const entry = this.settleEntry(raw);
129
+ if (entry === undefined) {
130
+ this.store.delete(qKey);
131
+ return undefined;
132
+ }
133
+ return (entry.value !== undefined ? structuredClone(entry.value) : undefined);
134
+ }
135
+ // 根命名空间精确未命中时回扫全部命名空间,与 core RuntimeStateStore 语义对齐
136
+ if (!this.namespace) {
137
+ return this.findAcrossNamespaces(key);
138
+ }
139
+ return undefined;
86
140
  }
87
141
  async set(key, value, ttl) {
88
142
  const qKey = this.qualify(key);
@@ -364,7 +418,8 @@ export function createTestRuntime(options = {}) {
364
418
  }
365
419
  }
366
420
  const events = new TestEventSink();
367
- const memoryLogger = options.logger instanceof MemoryLogger ? options.logger : new MemoryLogger();
421
+ // 类型已收窄为 MemoryLogger,直接使用注入实例或默认新建,不再做静默替换
422
+ const memoryLogger = options.logger ?? new MemoryLogger();
368
423
  const actionsMap = normalizeTestActions(options.actions, packageId);
369
424
  const executionService = new DefaultExecutionService({
370
425
  packageId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actiondock/testing",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "ActionDock Test Runtime for testing Actions with real Core execution semantics",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,8 +29,8 @@
29
29
  "test": "node ../../scripts/run-tests.ts"
30
30
  },
31
31
  "dependencies": {
32
- "@actiondock/core": "^2.2.1",
33
- "@actiondock/sdk": "^2.2.1"
32
+ "@actiondock/core": "^2.3.0",
33
+ "@actiondock/sdk": "^2.3.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "typescript": "^5.7.0"