@cosmicdrift/kumiko-framework 0.25.0 → 0.26.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -0,0 +1,94 @@
1
+ // createInMemoryFileProvider Unit-Tests (Phase 1, test-luecken-integration).
2
+ //
3
+ // Pinnt den FileStorageProvider-Contract der In-Memory-Impl — inkl. der
4
+ // non-obvious Eigenschaften: defensive Buffer-Copies (write UND read),
5
+ // lazy readStream-throw (erst beim ersten Chunk-Pull), und die bewusst
6
+ // erkennbare memory://-Fake-URL.
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import { createInMemoryFileProvider } from "../in-memory-provider";
10
+
11
+ const bytes = (s: string) => new TextEncoder().encode(s);
12
+ const decode = (u: Uint8Array) => new TextDecoder().decode(u);
13
+
14
+ describe("createInMemoryFileProvider — write/read roundtrip", () => {
15
+ test("read liefert die geschriebenen Bytes zurück", async () => {
16
+ const p = createInMemoryFileProvider();
17
+ await p.write("a.txt", bytes("hello"));
18
+ expect(decode(await p.read("a.txt"))).toBe("hello");
19
+ });
20
+
21
+ test("write kopiert defensiv — Caller-Mutation nach write ändert Storage nicht", async () => {
22
+ const p = createInMemoryFileProvider();
23
+ const data = bytes("orig");
24
+ await p.write("k", data);
25
+ data[0] = 0;
26
+ expect(decode(await p.read("k"))).toBe("orig");
27
+ });
28
+
29
+ test("read kopiert defensiv — Mutation des Ergebnisses ändert Storage nicht", async () => {
30
+ const p = createInMemoryFileProvider();
31
+ await p.write("k", bytes("orig"));
32
+ const first = await p.read("k");
33
+ first[0] = 0;
34
+ expect(decode(await p.read("k"))).toBe("orig");
35
+ });
36
+
37
+ test("read auf fehlenden Key wirft", async () => {
38
+ const p = createInMemoryFileProvider();
39
+ await expect(p.read("missing")).rejects.toThrow("in-memory file not found: missing");
40
+ });
41
+ });
42
+
43
+ describe("createInMemoryFileProvider — writeStream/readStream", () => {
44
+ test("writeStream fügt Chunks zusammen, readStream liest zurück", async () => {
45
+ const p = createInMemoryFileProvider();
46
+ async function* src() {
47
+ yield bytes("foo");
48
+ yield bytes("bar");
49
+ }
50
+ await p.writeStream("s", src());
51
+ let out = "";
52
+ for await (const chunk of p.readStream("s")) out += decode(chunk);
53
+ expect(out).toBe("foobar");
54
+ });
55
+
56
+ test("readStream auf fehlenden Key wirft erst beim ersten Chunk-Pull (lazy, wie S3)", async () => {
57
+ const p = createInMemoryFileProvider();
58
+ const it = p.readStream("missing")[Symbol.asyncIterator]();
59
+ await expect(it.next()).rejects.toThrow("in-memory file not found: missing");
60
+ });
61
+ });
62
+
63
+ describe("createInMemoryFileProvider — exists/delete", () => {
64
+ test("exists spiegelt write + delete", async () => {
65
+ const p = createInMemoryFileProvider();
66
+ expect(await p.exists("k")).toBe(false);
67
+ await p.write("k", bytes("x"));
68
+ expect(await p.exists("k")).toBe(true);
69
+ await p.delete("k");
70
+ expect(await p.exists("k")).toBe(false);
71
+ });
72
+
73
+ test("delete auf fehlenden Key ist no-op", async () => {
74
+ const p = createInMemoryFileProvider();
75
+ await expect(p.delete("nope")).resolves.toBeUndefined();
76
+ });
77
+ });
78
+
79
+ describe("createInMemoryFileProvider — getSignedUrl/keys/clear", () => {
80
+ test("getSignedUrl liefert deterministische memory://-Fake-URL", async () => {
81
+ const p = createInMemoryFileProvider();
82
+ expect(p.getSignedUrl).toBeDefined();
83
+ expect(await p.getSignedUrl?.("path/to/f.jpg", 300)).toBe("memory://path/to/f.jpg?expires=300");
84
+ });
85
+
86
+ test("keys listet geschriebene Keys, clear leert alles", async () => {
87
+ const p = createInMemoryFileProvider();
88
+ await p.write("a", bytes("1"));
89
+ await p.write("b", bytes("2"));
90
+ expect([...p.keys()].sort()).toEqual(["a", "b"]);
91
+ p.clear();
92
+ expect(p.keys()).toEqual([]);
93
+ });
94
+ });
@@ -0,0 +1,47 @@
1
+ // createFallbackLogger Unit-Tests (Phase 1, test-luecken-integration).
2
+ //
3
+ // Pinnt beide Pfade des Fallback-Loggers — inkl. des non-obvious
4
+ // Format-Unterschieds: der wrapped-Pfad schreibt "[ns] msg", der
5
+ // console-Fallback "[ns] msg:" (trailing colon).
6
+
7
+ import { describe, expect, mock, spyOn, test } from "bun:test";
8
+ import { createFallbackLogger } from "../utils";
9
+
10
+ describe("createFallbackLogger", () => {
11
+ describe("mit wrapped logger", () => {
12
+ test("delegiert an logger.error mit [namespace]-Prefix (kein colon)", () => {
13
+ const error = mock((_msg: string, _data?: Record<string, unknown>) => {});
14
+ const fallback = createFallbackLogger("redis", { error });
15
+
16
+ fallback.error("connection lost", { attempt: 3 });
17
+
18
+ expect(error).toHaveBeenCalledTimes(1);
19
+ expect(error).toHaveBeenCalledWith("[redis] connection lost", { attempt: 3 });
20
+ });
21
+
22
+ test("reicht fehlendes data-Argument als undefined durch", () => {
23
+ const error = mock((_msg: string, _data?: Record<string, unknown>) => {});
24
+ const fallback = createFallbackLogger("jobs", { error });
25
+
26
+ fallback.error("boom");
27
+
28
+ expect(error).toHaveBeenCalledWith("[jobs] boom", undefined);
29
+ });
30
+ });
31
+
32
+ describe("ohne logger (console-Fallback)", () => {
33
+ test("schreibt auf console.error mit [namespace]-Prefix UND trailing colon", () => {
34
+ const spy = spyOn(console, "error").mockImplementation(() => {});
35
+ try {
36
+ const fallback = createFallbackLogger("boot");
37
+
38
+ fallback.error("no logger wired", { phase: "init" });
39
+
40
+ expect(spy).toHaveBeenCalledTimes(1);
41
+ expect(spy).toHaveBeenCalledWith("[boot] no logger wired:", { phase: "init" });
42
+ } finally {
43
+ spy.mockRestore();
44
+ }
45
+ });
46
+ });
47
+ });
@@ -0,0 +1,44 @@
1
+ // words.ts Invarianten-Tests (Phase 1, test-luecken-integration).
2
+ //
3
+ // Schützt die Slug-Wortlisten gegen fehlerhafte Edits (Duplikate,
4
+ // Großbuchstaben, Bindestriche, Müll-Einträge). Bewusst NICHT an die
5
+ // veralteten Inline-Kommentare gebunden ("150 × 150", "4-8 Buchstaben") —
6
+ // real sind es mehr Wörter und weitere Längen; getestet werden die echten
7
+ // harten Invarianten + die dokumentierte Mindest-Diversität.
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+ import { ADJECTIVES, NOUNS } from "../index";
11
+
12
+ const LISTS: ReadonlyArray<readonly [string, readonly string[]]> = [
13
+ ["ADJECTIVES", ADJECTIVES],
14
+ ["NOUNS", NOUNS],
15
+ ];
16
+
17
+ describe("words — Slug-Wortlisten Invarianten", () => {
18
+ for (const [name, list] of LISTS) {
19
+ describe(name, () => {
20
+ test("nur lowercase a-z (keine Ziffern, Bindestriche, Whitespace, Umlaute)", () => {
21
+ const offenders = list.filter((w) => !/^[a-z]+$/.test(w));
22
+ expect(offenders).toEqual([]);
23
+ });
24
+
25
+ test("keine Duplikate", () => {
26
+ const dupes = list.filter((w, i) => list.indexOf(w) !== i);
27
+ expect(dupes).toEqual([]);
28
+ });
29
+
30
+ test("mindestens 150 Wörter (untere Schranke für Combo-Diversität)", () => {
31
+ expect(list.length).toBeGreaterThanOrEqual(150);
32
+ });
33
+
34
+ test("Wortlänge im Müll-Schutz-Korridor 3..12 (fängt leere/Satz-Einträge)", () => {
35
+ const outliers = list.filter((w) => w.length < 3 || w.length > 12);
36
+ expect(outliers).toEqual([]);
37
+ });
38
+ });
39
+ }
40
+
41
+ test("ergibt ≥ 22.500 saubere Kombinationen (dokumentierte Mindest-Diversität)", () => {
42
+ expect(ADJECTIVES.length * NOUNS.length).toBeGreaterThanOrEqual(22_500);
43
+ });
44
+ });
@@ -42,9 +42,9 @@ export type AdjNounNameOptions = {
42
42
  * (no-confusable-Alphabet). Empfohlen 3 Zeichen = 32^3 = 32.768
43
43
  * zusätzliche Combinations pro Wortpaar. */
44
44
  readonly suffix?: { readonly length: number };
45
- /** Custom Adjective-Liste — default ADJECTIVES (150 generic). */
45
+ /** Custom Adjective-Liste — default ADJECTIVES (191 generic). */
46
46
  readonly adjectives?: readonly string[];
47
- /** Custom Noun-Liste — default NOUNS (150 generic). Apps die Domain-
47
+ /** Custom Noun-Liste — default NOUNS (173 generic). Apps die Domain-
48
48
  * spezifische Slugs wollen (z.B. webhook-feature mit eigenen
49
49
  * -receiver/-listener-Substantiven) reichen ihre eigene Liste. */
50
50
  readonly nouns?: readonly string[];
@@ -83,7 +83,7 @@ export type GenerateUniqueNameOptions = {
83
83
  * ist (typisch: DB-Query "select where slug=$1" → row count === 0). */
84
84
  readonly isAvailable: (name: string) => Promise<boolean>;
85
85
  /** Max Versuche OHNE Suffix bevor wir auf suffix-mode wechseln.
86
- * Default 3. Bei 22.500 Default-Combos und ~150 existierenden
86
+ * Default 3. Bei 33.043 Default-Combos und ~150 existierenden
87
87
  * Tenants liegt p(Kollision) < 1% — 3 Versuche reichen weit. */
88
88
  readonly maxCleanAttempts?: number;
89
89
  /** Suffix-Länge bei Kollision-Mode. Default 3 (= 32.768 Combinations
@@ -9,12 +9,12 @@
9
9
  // - Keine Personennamen (cultural appropriation, prominenten-collision)
10
10
  // - Keine Themen-Cluster (kein Wetter-only, kein Geographie-only)
11
11
  // - Lowercase, ASCII-only, keine Bindestriche im Wort selbst
12
- // - 4-8 Buchstaben pro Wort (kompakter Slug)
12
+ // - 3-10 Buchstaben pro Wort (kompakter Slug)
13
13
  // - Aussprechbar in Deutsch UND Englisch (User-Telefon-Support)
14
14
  // - Keine Wörter mit ambiguer Bedeutung in Englisch+Deutsch
15
15
  //
16
- // 150 × 150 = 22.500 saubere Kombinationen — bei einer Standard-
17
- // Hashing-Kollision (Birthday-Bound) reicht das für ~150 Tenants ohne
16
+ // 191 × 173 = 33.043 saubere Kombinationen — bei einer Standard-
17
+ // Hashing-Kollision (Birthday-Bound) reicht das für ~180 Tenants ohne
18
18
  // Suffix. Drüber kommt der Suffix-Pfad in generateUniqueName.
19
19
  //
20
20
  // Erweiterung: weitere Wörter unten anhängen reicht (sortiert ist