@cosmicdrift/kumiko-framework 0.2.2 → 0.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.
Files changed (191) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/package.json +124 -38
  3. package/src/__tests__/full-stack.integration.ts +2 -2
  4. package/src/api/auth-routes.ts +5 -5
  5. package/src/api/jwt.ts +2 -2
  6. package/src/api/route-registrars.ts +1 -1
  7. package/src/api/routes.ts +3 -3
  8. package/src/api/server.ts +6 -7
  9. package/src/auth/__tests__/roles.test.ts +24 -0
  10. package/src/auth/index.ts +7 -0
  11. package/src/auth/roles.ts +42 -0
  12. package/src/compliance/__tests__/duration-spec.test.ts +72 -0
  13. package/src/compliance/__tests__/profiles.test.ts +308 -0
  14. package/src/compliance/__tests__/sub-processors.test.ts +139 -0
  15. package/src/compliance/duration-spec.ts +44 -0
  16. package/src/compliance/index.ts +31 -0
  17. package/src/compliance/override-schema.ts +136 -0
  18. package/src/compliance/profiles.ts +427 -0
  19. package/src/compliance/sub-processors.ts +152 -0
  20. package/src/db/__tests__/big-int-field.test.ts +131 -0
  21. package/src/db/assert-exists-in.ts +2 -2
  22. package/src/db/cursor.ts +3 -3
  23. package/src/db/event-store-executor.ts +19 -13
  24. package/src/db/located-timestamp.ts +1 -1
  25. package/src/db/money.ts +12 -2
  26. package/src/db/pg-error.ts +1 -1
  27. package/src/db/row-helpers.ts +1 -1
  28. package/src/db/table-builder.ts +20 -5
  29. package/src/db/tenant-db.ts +9 -9
  30. package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
  31. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
  32. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
  33. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -0
  34. package/src/engine/__tests__/build-target.test.ts +135 -0
  35. package/src/engine/__tests__/codemod-pipeline.test.ts +551 -0
  36. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  37. package/src/engine/__tests__/event-helpers.test.ts +4 -4
  38. package/src/engine/__tests__/pipeline-engine.test.ts +215 -0
  39. package/src/engine/__tests__/pipeline-handler.integration.ts +894 -0
  40. package/src/engine/__tests__/pipeline-observability.integration.ts +142 -0
  41. package/src/engine/__tests__/pipeline-performance.integration.ts +152 -0
  42. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +288 -0
  43. package/src/engine/__tests__/raw-table.test.ts +2 -2
  44. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +115 -0
  45. package/src/engine/__tests__/steps-aggregate-create.test.ts +92 -0
  46. package/src/engine/__tests__/steps-aggregate-update.test.ts +127 -0
  47. package/src/engine/__tests__/steps-call-feature.test.ts +123 -0
  48. package/src/engine/__tests__/steps-mail-send.test.ts +136 -0
  49. package/src/engine/__tests__/steps-read.test.ts +142 -0
  50. package/src/engine/__tests__/steps-resolver-utils.test.ts +50 -0
  51. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +69 -0
  52. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +117 -0
  53. package/src/engine/__tests__/steps-webhook-send.test.ts +135 -0
  54. package/src/engine/__tests__/steps-workflow.test.ts +198 -0
  55. package/src/engine/__tests__/validate-projection-allowlist.test.ts +491 -0
  56. package/src/engine/__tests__/visual-tree-patterns.test.ts +251 -0
  57. package/src/engine/boot-validator/api-ext.ts +77 -0
  58. package/src/engine/boot-validator/config-deps.ts +163 -0
  59. package/src/engine/boot-validator/entity-handler.ts +466 -0
  60. package/src/engine/boot-validator/index.ts +159 -0
  61. package/src/engine/boot-validator/ownership.ts +198 -0
  62. package/src/engine/boot-validator/pii-retention.ts +155 -0
  63. package/src/engine/boot-validator/screens-nav.ts +624 -0
  64. package/src/engine/boot-validator.ts +1 -1528
  65. package/src/engine/build-app-schema.ts +1 -1
  66. package/src/engine/build-target.ts +99 -0
  67. package/src/engine/codemod/index.ts +15 -0
  68. package/src/engine/codemod/pipeline-codemod.ts +641 -0
  69. package/src/engine/config-helpers.ts +9 -19
  70. package/src/engine/constants.ts +1 -1
  71. package/src/engine/define-feature.ts +127 -9
  72. package/src/engine/define-handler.ts +89 -3
  73. package/src/engine/define-roles.ts +2 -2
  74. package/src/engine/define-step.ts +28 -0
  75. package/src/engine/define-workflow.ts +110 -0
  76. package/src/engine/entity-handlers.ts +10 -9
  77. package/src/engine/event-helpers.ts +4 -4
  78. package/src/engine/extension-names.ts +105 -0
  79. package/src/engine/extensions/user-data.ts +106 -0
  80. package/src/engine/factories.ts +26 -16
  81. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +184 -0
  82. package/src/engine/feature-ast/extractors/index.ts +74 -0
  83. package/src/engine/feature-ast/extractors/round1.ts +110 -0
  84. package/src/engine/feature-ast/extractors/round2.ts +253 -0
  85. package/src/engine/feature-ast/extractors/round3.ts +471 -0
  86. package/src/engine/feature-ast/extractors/round4.ts +1365 -0
  87. package/src/engine/feature-ast/extractors/round5.ts +72 -0
  88. package/src/engine/feature-ast/extractors/round6.ts +66 -0
  89. package/src/engine/feature-ast/extractors/shared.ts +177 -0
  90. package/src/engine/feature-ast/parse.ts +13 -0
  91. package/src/engine/feature-ast/patch.ts +9 -1
  92. package/src/engine/feature-ast/patcher.ts +10 -3
  93. package/src/engine/feature-ast/patterns.ts +71 -1
  94. package/src/engine/feature-ast/render.ts +31 -1
  95. package/src/engine/index.ts +66 -2
  96. package/src/engine/pattern-library/__tests__/library.test.ts +11 -0
  97. package/src/engine/pattern-library/library.ts +78 -2
  98. package/src/engine/pipeline.ts +88 -0
  99. package/src/engine/projection-helpers.ts +1 -1
  100. package/src/engine/read-claim.ts +1 -1
  101. package/src/engine/registry.ts +30 -2
  102. package/src/engine/resolve-config-or-param.ts +4 -0
  103. package/src/engine/run-pipeline.ts +162 -0
  104. package/src/engine/schema-builder.ts +10 -4
  105. package/src/engine/state-machine.ts +1 -1
  106. package/src/engine/steps/_drizzle-boundary.ts +19 -0
  107. package/src/engine/steps/_duration-utils.ts +33 -0
  108. package/src/engine/steps/_no-return-guard.ts +21 -0
  109. package/src/engine/steps/_resolver-utils.ts +42 -0
  110. package/src/engine/steps/_step-dispatch-constants.ts +38 -0
  111. package/src/engine/steps/aggregate-append-event.ts +56 -0
  112. package/src/engine/steps/aggregate-create.ts +56 -0
  113. package/src/engine/steps/aggregate-update.ts +68 -0
  114. package/src/engine/steps/branch.ts +84 -0
  115. package/src/engine/steps/call-feature.ts +49 -0
  116. package/src/engine/steps/compute.ts +41 -0
  117. package/src/engine/steps/for-each.ts +111 -0
  118. package/src/engine/steps/mail-send.ts +44 -0
  119. package/src/engine/steps/read-find-many.ts +51 -0
  120. package/src/engine/steps/read-find-one.ts +58 -0
  121. package/src/engine/steps/retry.ts +87 -0
  122. package/src/engine/steps/return.ts +34 -0
  123. package/src/engine/steps/unsafe-projection-delete.ts +46 -0
  124. package/src/engine/steps/unsafe-projection-upsert.ts +69 -0
  125. package/src/engine/steps/wait-for-event.ts +71 -0
  126. package/src/engine/steps/wait.ts +69 -0
  127. package/src/engine/steps/webhook-send.ts +71 -0
  128. package/src/engine/system-user.ts +1 -1
  129. package/src/engine/types/feature.ts +143 -1
  130. package/src/engine/types/fields.ts +134 -10
  131. package/src/engine/types/handlers.ts +18 -10
  132. package/src/engine/types/identifiers.ts +1 -0
  133. package/src/engine/types/index.ts +15 -1
  134. package/src/engine/types/step.ts +334 -0
  135. package/src/engine/types/target-ref.ts +21 -0
  136. package/src/engine/types/tree-node.ts +130 -0
  137. package/src/engine/types/workspace.ts +7 -0
  138. package/src/engine/validate-projection-allowlist.ts +161 -0
  139. package/src/event-store/snapshot.ts +1 -1
  140. package/src/event-store/upcaster-dead-letter.ts +1 -1
  141. package/src/event-store/upcaster.ts +1 -1
  142. package/src/files/__tests__/read-stream.test.ts +105 -0
  143. package/src/files/__tests__/write-stream.test.ts +233 -0
  144. package/src/files/__tests__/zip-stream.test.ts +357 -0
  145. package/src/files/file-routes.ts +1 -1
  146. package/src/files/in-memory-provider.ts +38 -0
  147. package/src/files/index.ts +3 -0
  148. package/src/files/local-provider.ts +58 -1
  149. package/src/files/types.ts +36 -8
  150. package/src/files/zip-stream.ts +251 -0
  151. package/src/jobs/job-runner.ts +10 -10
  152. package/src/lifecycle/lifecycle.ts +0 -3
  153. package/src/logging/index.ts +1 -0
  154. package/src/logging/pino-logger.ts +11 -7
  155. package/src/logging/utils.ts +24 -0
  156. package/src/observability/prometheus-meter.ts +7 -5
  157. package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
  158. package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
  159. package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
  160. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
  161. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
  162. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
  163. package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
  164. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
  165. package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
  166. package/src/pipeline/append-event-core.ts +22 -6
  167. package/src/pipeline/dispatcher-utils.ts +188 -0
  168. package/src/pipeline/dispatcher.ts +63 -283
  169. package/src/pipeline/distributed-lock.ts +1 -1
  170. package/src/pipeline/entity-cache.ts +2 -2
  171. package/src/pipeline/event-consumer-state.ts +0 -13
  172. package/src/pipeline/event-dispatcher.ts +4 -4
  173. package/src/pipeline/index.ts +0 -2
  174. package/src/pipeline/lifecycle-pipeline.ts +6 -12
  175. package/src/pipeline/msp-rebuild.ts +5 -5
  176. package/src/pipeline/multi-stream-apply-context.ts +6 -7
  177. package/src/pipeline/projection-rebuild.ts +2 -2
  178. package/src/pipeline/projection-state.ts +0 -12
  179. package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
  180. package/src/rate-limit/resolver.ts +1 -1
  181. package/src/search/in-memory-adapter.ts +1 -1
  182. package/src/search/meilisearch-adapter.ts +3 -3
  183. package/src/search/types.ts +1 -1
  184. package/src/secrets/leak-guard.ts +2 -2
  185. package/src/stack/request-helper.ts +9 -5
  186. package/src/stack/test-stack.ts +1 -1
  187. package/src/testing/handler-context.ts +4 -4
  188. package/src/testing/http-cookies.ts +1 -1
  189. package/src/time/tz-context.ts +1 -2
  190. package/src/ui-types/index.ts +4 -0
  191. package/src/engine/feature-ast/extractors.ts +0 -2562
@@ -115,5 +115,5 @@ export async function listDeadLetters(
115
115
  .from(upcasterDeadLetterTable)
116
116
  .orderBy(desc(upcasterDeadLetterTable.createdAt))
117
117
  .limit(limit);
118
- return rows as readonly DeadLetterRow[];
118
+ return rows as readonly DeadLetterRow[]; // @cast-boundary db-row
119
119
  }
@@ -88,7 +88,7 @@ async function upcastStoredEventWithPolicy(
88
88
  if (!info) return event;
89
89
  if (event.eventVersion >= info.currentVersion) return event;
90
90
 
91
- let payload = event.payload as unknown;
91
+ let payload = event.payload as unknown; // @cast-boundary engine-payload
92
92
  let v = event.eventVersion;
93
93
  const startVersion = event.eventVersion;
94
94
  while (v < info.currentVersion) {
@@ -0,0 +1,105 @@
1
+ // Storage-Provider readStream-API (S2.U3 Atom 3c.fix).
2
+ //
3
+ // Pinst:
4
+ // - In-memory-Provider: readStream yieldet die Bytes als single-chunk,
5
+ // Roundtrip identisch zu write+read.
6
+ // - Local-Provider: readStream nutzt fs.createReadStream → mehrere
7
+ // chunks fuer >hwm-Files. Lazy-Fail: ENOENT trifft erst beim ersten
8
+ // chunk-pull, nicht beim readStream-Aufruf.
9
+ // - Beide Provider: missing-key throw't beim Konsum, nicht bei
10
+ // readStream() — gleiches Lazy-Pattern wie S3.
11
+ //
12
+ // readStream + writeStream sind ab Atom 3c.fix REQUIRED in der Provider-
13
+ // Surface (kein optional). Der Type-Compiler erzwingt Implementierung,
14
+ // kein silent runtime-throw mehr.
15
+
16
+ import { mkdtemp, rm } from "node:fs/promises";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
20
+ import { createInMemoryFileProvider } from "../in-memory-provider";
21
+ import { createLocalProvider } from "../local-provider";
22
+
23
+ async function collect(stream: AsyncIterable<Uint8Array>): Promise<Uint8Array> {
24
+ const chunks: Uint8Array[] = [];
25
+ let total = 0;
26
+ for await (const chunk of stream) {
27
+ chunks.push(chunk);
28
+ total += chunk.byteLength;
29
+ }
30
+ const out = new Uint8Array(total);
31
+ let offset = 0;
32
+ for (const c of chunks) {
33
+ out.set(c, offset);
34
+ offset += c.byteLength;
35
+ }
36
+ return out;
37
+ }
38
+
39
+ describe("FileStorageProvider.readStream — in-memory", () => {
40
+ test("readStream returnt geschriebene Bytes identisch", async () => {
41
+ const provider = createInMemoryFileProvider();
42
+ const data = new Uint8Array([1, 2, 3, 4, 5]);
43
+ await provider.write("test/foo.bin", data);
44
+
45
+ const result = await collect(provider.readStream("test/foo.bin"));
46
+ expect(Array.from(result)).toEqual([1, 2, 3, 4, 5]);
47
+ });
48
+
49
+ test("missing-key throw't beim ersten chunk-pull (lazy-Pattern)", async () => {
50
+ const provider = createInMemoryFileProvider();
51
+ // readStream() selbst wirft NICHT — das Iterator-Object existiert.
52
+ const stream = provider.readStream("does/not/exist.bin");
53
+ expect(stream).toBeDefined();
54
+ // Erst beim Iterieren faellt der Fehler.
55
+ await expect(collect(stream)).rejects.toThrow(/in-memory file not found/);
56
+ });
57
+ });
58
+
59
+ describe("FileStorageProvider.readStream — local", () => {
60
+ let basePath: string;
61
+
62
+ beforeEach(async () => {
63
+ basePath = await mkdtemp(join(tmpdir(), "kumiko-readstream-test-"));
64
+ });
65
+
66
+ afterEach(async () => {
67
+ await rm(basePath, { recursive: true, force: true });
68
+ });
69
+
70
+ test("readStream returnt geschriebene Bytes identisch", async () => {
71
+ const provider = createLocalProvider(basePath);
72
+ const data = new Uint8Array([10, 20, 30, 40, 50]);
73
+ await provider.write("foo.bin", data);
74
+
75
+ const result = await collect(provider.readStream("foo.bin"));
76
+ expect(Array.from(result)).toEqual([10, 20, 30, 40, 50]);
77
+ });
78
+
79
+ test("readStream chunkt grosse Files (>64KB highWaterMark)", async () => {
80
+ // 200KB > default highWaterMark=64KB → erwartet mind. 2 chunks.
81
+ const provider = createLocalProvider(basePath);
82
+ const big = new Uint8Array(200 * 1024);
83
+ for (let i = 0; i < big.length; i++) big[i] = i & 0xff;
84
+ await provider.write("big.bin", big);
85
+
86
+ let chunkCount = 0;
87
+ let totalBytes = 0;
88
+ for await (const chunk of provider.readStream("big.bin")) {
89
+ chunkCount++;
90
+ totalBytes += chunk.byteLength;
91
+ }
92
+ expect(chunkCount).toBeGreaterThan(1);
93
+ expect(totalBytes).toBe(200 * 1024);
94
+ });
95
+
96
+ test("ENOENT throw't beim ersten chunk-pull (lazy-Pattern)", async () => {
97
+ // Pinst dass readStream() selbst kein Filesystem-Lookup macht;
98
+ // node:fs createReadStream emittiert error event erst beim Read-
99
+ // Versuch. Test-Coverage matched die Inmemory-Variante.
100
+ const provider = createLocalProvider(basePath);
101
+ const stream = provider.readStream("does/not/exist.bin");
102
+ expect(stream).toBeDefined();
103
+ await expect(collect(stream)).rejects.toThrow(/ENOENT|no such file/);
104
+ });
105
+ });
@@ -0,0 +1,233 @@
1
+ // Storage-Provider writeStream-API (S2.U3 Atom 1d).
2
+ //
3
+ // Pinst:
4
+ // - In-memory-Provider: writeStream collected chunks + macht
5
+ // read-Roundtrip identisch zu write(uint8array).
6
+ // - Local-Provider: writeStream schreibt atomar via tmp + rename,
7
+ // halb-fertige Stream-Bricht hinterlassen keine Garbage am Final-
8
+ // Pfad.
9
+ // - Beide Provider liefern die Bytes via read() identisch zurueck —
10
+ // kein chunk-Loss, kein chunk-Order-Verlust.
11
+ //
12
+ // AsyncIterable-source pinst die Streaming-Semantik (Caller streamt
13
+ // chunk-fuer-chunk, Provider niemals alles im Memory).
14
+
15
+ import { mkdtemp, readdir, rm, stat } from "node:fs/promises";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
19
+ import { createInMemoryFileProvider } from "../in-memory-provider";
20
+ import { createLocalProvider } from "../local-provider";
21
+
22
+ async function* fromChunks(chunks: Uint8Array[]): AsyncIterable<Uint8Array> {
23
+ for (const c of chunks) {
24
+ yield c;
25
+ }
26
+ }
27
+
28
+ async function* slowChunks(chunks: Uint8Array[], delayMs: number): AsyncIterable<Uint8Array> {
29
+ for (const c of chunks) {
30
+ await new Promise((r) => setTimeout(r, delayMs));
31
+ yield c;
32
+ }
33
+ }
34
+
35
+ describe("FileStorageProvider.writeStream — in-memory", () => {
36
+ test("schreibt 3 chunks + read liefert konkateniert zurueck", async () => {
37
+ const provider = createInMemoryFileProvider();
38
+ const chunks = [
39
+ new Uint8Array([1, 2, 3]),
40
+ new Uint8Array([4, 5, 6]),
41
+ new Uint8Array([7, 8, 9]),
42
+ ];
43
+ await provider.writeStream("test/file.bin", fromChunks(chunks));
44
+
45
+ const data = await provider.read("test/file.bin");
46
+ expect(Array.from(data)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
47
+ });
48
+
49
+ test("leerer Stream → leere Datei", async () => {
50
+ const provider = createInMemoryFileProvider();
51
+ await provider.writeStream("empty.bin", fromChunks([]));
52
+
53
+ const data = await provider.read("empty.bin");
54
+ expect(data.byteLength).toBe(0);
55
+ });
56
+
57
+ test("mimeType-Option wird in der gespeicherten Entry erhalten", async () => {
58
+ // Kein direkter Read-Pfad fuer mimeType — wir checken via existing-
59
+ // Marker (write-Pfad teilt den storage-shape). Symmetrie-Test:
60
+ // ein write() + ein writeStream() mit gleichem key + gleichem
61
+ // mimeType muessen byte-identische Reads liefern.
62
+ const a = createInMemoryFileProvider();
63
+ const b = createInMemoryFileProvider();
64
+ const bytes = new Uint8Array([42, 43, 44]);
65
+
66
+ await a.write("k", bytes, "application/zip");
67
+ if (!b.writeStream) throw new Error("writeStream missing");
68
+ await b.writeStream("k", fromChunks([bytes]), { mimeType: "application/zip" });
69
+
70
+ expect(Array.from(await a.read("k"))).toEqual(Array.from(await b.read("k")));
71
+ });
72
+ });
73
+
74
+ describe("FileStorageProvider.writeStream — local-filesystem", () => {
75
+ let basePath: string;
76
+
77
+ beforeEach(async () => {
78
+ basePath = await mkdtemp(join(tmpdir(), "kumiko-write-stream-"));
79
+ });
80
+
81
+ afterEach(async () => {
82
+ await rm(basePath, { recursive: true, force: true });
83
+ });
84
+
85
+ test("schreibt + read roundtrip mit chunked source", async () => {
86
+ const provider = createLocalProvider(basePath);
87
+ const chunks = [new Uint8Array([10, 20, 30]), new Uint8Array([40, 50, 60])];
88
+ await provider.writeStream("dir/foo.bin", fromChunks(chunks));
89
+
90
+ const data = await provider.read("dir/foo.bin");
91
+ expect(Array.from(data)).toEqual([10, 20, 30, 40, 50, 60]);
92
+ });
93
+
94
+ test("legt parent-Verzeichnisse rekursiv an", async () => {
95
+ const provider = createLocalProvider(basePath);
96
+ await provider.writeStream("deeply/nested/path/file.bin", fromChunks([new Uint8Array([1])]));
97
+
98
+ const stats = await stat(join(basePath, "deeply/nested/path/file.bin"));
99
+ expect(stats.isFile()).toBe(true);
100
+ });
101
+
102
+ test("atomar: bei wirfendem Stream entsteht KEIN final-Pfad-File", async () => {
103
+ const provider = createLocalProvider(basePath);
104
+
105
+ async function* failingSource(): AsyncIterable<Uint8Array> {
106
+ yield new Uint8Array([1, 2, 3]);
107
+ throw new Error("synthetic mid-stream failure");
108
+ }
109
+
110
+ await expect(provider.writeStream("dir/half.bin", failingSource())).rejects.toThrow(
111
+ /synthetic mid-stream failure/,
112
+ );
113
+
114
+ expect(await provider.exists("dir/half.bin")).toBe(false);
115
+ });
116
+
117
+ test("atomar: nach Failure ist KEIN final-Pfad-File da (.tmp-Leak ist best-effort)", async () => {
118
+ // Echte Atomicity-Garantie: der final-Pfad ist niemals halb-fertig
119
+ // sichtbar. tmp-Files koennen je nach OS-Race im destroy-Pfad
120
+ // kurz liegen bleiben — kein Korrektheitsproblem (kein Reader
121
+ // sucht nach `*.tmp`-Patterns), nur Operations-Hygiene.
122
+ const provider = createLocalProvider(basePath);
123
+
124
+ async function* failing(): AsyncIterable<Uint8Array> {
125
+ yield new Uint8Array([99]);
126
+ throw new Error("fail");
127
+ }
128
+
129
+ await expect(provider.writeStream("subdir/leak-check.bin", failing())).rejects.toThrow();
130
+
131
+ // Final-Pfad ist NICHT da — das ist die harte Garantie.
132
+ expect(await provider.exists("subdir/leak-check.bin")).toBe(false);
133
+
134
+ // tmp-Files duerfen nicht den final-Namen haben (sonst kein Atomicity).
135
+ const entries = await readdir(join(basePath, "subdir"));
136
+ for (const entry of entries) {
137
+ expect(entry).not.toBe("leak-check.bin");
138
+ }
139
+ });
140
+
141
+ test("ueberschreibt existing file atomar (rename ueber bestehenden Pfad)", async () => {
142
+ const provider = createLocalProvider(basePath);
143
+
144
+ await provider.write("k", new Uint8Array([1, 1, 1]));
145
+ await provider.writeStream("k", fromChunks([new Uint8Array([2, 2, 2])]));
146
+
147
+ const data = await provider.read("k");
148
+ expect(Array.from(data)).toEqual([2, 2, 2]);
149
+ });
150
+ });
151
+
152
+ describe("FileStorageProvider.writeStream — Streaming-Property", () => {
153
+ // Pinst dass writeStream tatsaechlich AsyncIterable konsumiert ohne
154
+ // alle chunks in eine Promise.resolve(...) Array zu converten.
155
+ // Wenn ein Provider intern das chunks-Array sammelt, ist das fuer
156
+ // unsere semantischen Garantien OK — wir testen nur das Resultat.
157
+ // Dieser Test sichert dass der Caller ein REIN async-iterable
158
+ // uebergeben kann (z.B. ZIP-Stream der chunks lazy generiert).
159
+
160
+ test("Source mit Promise-delays wird korrekt verarbeitet", async () => {
161
+ const provider = createInMemoryFileProvider();
162
+
163
+ async function* lazySource(): AsyncIterable<Uint8Array> {
164
+ await new Promise((r) => setTimeout(r, 5));
165
+ yield new Uint8Array([1]);
166
+ await new Promise((r) => setTimeout(r, 5));
167
+ yield new Uint8Array([2]);
168
+ await new Promise((r) => setTimeout(r, 5));
169
+ yield new Uint8Array([3]);
170
+ }
171
+
172
+ await provider.writeStream("lazy.bin", lazySource());
173
+
174
+ const data = await provider.read("lazy.bin");
175
+ expect(Array.from(data)).toEqual([1, 2, 3]);
176
+ });
177
+
178
+ test("local-Provider streamt WAEHREND der Source yieldet (tmp-File existiert pre-completion)", async () => {
179
+ // Echter Streaming-Property-Test: bei langsamen Source-yields muss
180
+ // der local-Provider die tmp-File anfangen zu schreiben WAEHREND
181
+ // wir noch chunks zur Verfuegung stellen — nicht erst alle chunks
182
+ // collecten.
183
+ //
184
+ // Pattern: 5 chunks mit je 30ms delay zwischen yields. Wir starten
185
+ // writeStream + pollen alle 10ms (max 200ms) bis tmp-File auftaucht.
186
+ // Bei collect-then-write taucht NIE eine tmp-File auf — Test failed
187
+ // dann via Timeout. Poll-basiert statt time-hardcoded gibt CI-
188
+ // Geschwindigkeits-Toleranz; flake-frei solange total-source-time
189
+ // (5 × 30ms = 150ms) > poll-Granularitaet (10ms).
190
+ const basePath = await mkdtemp(join(tmpdir(), "kumiko-stream-prop-"));
191
+ try {
192
+ const provider = createLocalProvider(basePath);
193
+ const chunks = [
194
+ new Uint8Array([1]),
195
+ new Uint8Array([2]),
196
+ new Uint8Array([3]),
197
+ new Uint8Array([4]),
198
+ new Uint8Array([5]),
199
+ ];
200
+ const writePromise = provider.writeStream("streamed.bin", slowChunks(chunks, 30));
201
+
202
+ // Poll bis tmp existiert ODER final schon fertig (zu schneller CI).
203
+ let hasTmp = false;
204
+ let alreadyDone = false;
205
+ for (let i = 0; i < 20 && !hasTmp && !alreadyDone; i++) {
206
+ await new Promise((r) => setTimeout(r, 10));
207
+ const dirContents = await readdir(basePath);
208
+ hasTmp = dirContents.some((f) => f.endsWith(".tmp"));
209
+ alreadyDone = dirContents.includes("streamed.bin");
210
+ }
211
+ if (alreadyDone && !hasTmp) {
212
+ throw new Error(
213
+ "slowChunks-delay zu kurz fuer CI: writeStream war fertig bevor poll start. " +
214
+ "delayMs erhoehen oder chunks reduzieren.",
215
+ );
216
+ }
217
+ expect(hasTmp).toBe(true); // tmp existiert WAEHREND yields
218
+
219
+ // Warten auf completion
220
+ await writePromise;
221
+
222
+ // Nach completion: tmp ist weg, final-File ist da
223
+ const finalContents = await readdir(basePath);
224
+ expect(finalContents).toContain("streamed.bin");
225
+ expect(finalContents.filter((f) => f.endsWith(".tmp"))).toEqual([]);
226
+
227
+ const data = await provider.read("streamed.bin");
228
+ expect(Array.from(data)).toEqual([1, 2, 3, 4, 5]);
229
+ } finally {
230
+ await rm(basePath, { recursive: true, force: true });
231
+ }
232
+ });
233
+ });