@c9up/spectrum 0.1.3
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/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/Logger.d.ts +34 -0
- package/dist/Logger.d.ts.map +1 -0
- package/dist/Logger.js +72 -0
- package/dist/Logger.js.map +1 -0
- package/dist/RustLogBridge.d.ts +22 -0
- package/dist/RustLogBridge.d.ts.map +1 -0
- package/dist/RustLogBridge.js +124 -0
- package/dist/RustLogBridge.js.map +1 -0
- package/dist/SpectrumProvider.d.ts +19 -0
- package/dist/SpectrumProvider.d.ts.map +1 -0
- package/dist/SpectrumProvider.js +35 -0
- package/dist/SpectrumProvider.js.map +1 -0
- package/dist/channels/ConsoleChannel.d.ts +13 -0
- package/dist/channels/ConsoleChannel.d.ts.map +1 -0
- package/dist/channels/ConsoleChannel.js +89 -0
- package/dist/channels/ConsoleChannel.js.map +1 -0
- package/dist/channels/FileChannel.d.ts +23 -0
- package/dist/channels/FileChannel.d.ts.map +1 -0
- package/dist/channels/FileChannel.js +150 -0
- package/dist/channels/FileChannel.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -0
- package/dist/configure.d.ts +10 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +11 -0
- package/dist/configure.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/services/main.d.ts +18 -0
- package/dist/services/main.d.ts.map +1 -0
- package/dist/services/main.js +31 -0
- package/dist/services/main.js.map +1 -0
- package/dist/testing/FakeLogger.d.ts +28 -0
- package/dist/testing/FakeLogger.d.ts.map +1 -0
- package/dist/testing/FakeLogger.js +111 -0
- package/dist/testing/FakeLogger.js.map +1 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/package.json +67 -0
- package/src/Logger.ts +99 -0
- package/src/RustLogBridge.ts +142 -0
- package/src/SpectrumProvider.ts +52 -0
- package/src/channels/ConsoleChannel.ts +100 -0
- package/src/channels/FileChannel.ts +170 -0
- package/src/config.ts +7 -0
- package/src/configure.ts +23 -0
- package/src/index.ts +13 -0
- package/src/services/main.ts +39 -0
- package/src/testing/FakeLogger.ts +140 -0
- package/src/types.ts +36 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory `LogChannel` for tests — captures every `write(entry)`
|
|
3
|
+
* and exposes Adonis/Laravel-style `assertLogged` /
|
|
4
|
+
* `assertNotLogged` helpers in the same shape as Rover's
|
|
5
|
+
* `FakeMail` and Bay's `FakeQueue`.
|
|
6
|
+
*
|
|
7
|
+
* Not re-exported from the main barrel; reach via
|
|
8
|
+
* `@c9up/spectrum/testing`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { LogChannel, LogEntry, LogLevel } from "../types.js";
|
|
12
|
+
|
|
13
|
+
export interface FakeLoggerPredicate {
|
|
14
|
+
/** Substring match against `entry.message`. */
|
|
15
|
+
containing?: string;
|
|
16
|
+
/** Custom predicate against `entry.data`. */
|
|
17
|
+
dataMatches?: (data: Record<string, unknown> | undefined) => boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type FakeLoggerPredicateArg =
|
|
21
|
+
| FakeLoggerPredicate
|
|
22
|
+
| ((entry: LogEntry) => boolean);
|
|
23
|
+
|
|
24
|
+
export class FakeLogger implements LogChannel {
|
|
25
|
+
readonly name = "fake";
|
|
26
|
+
#captured: LogEntry[] = [];
|
|
27
|
+
|
|
28
|
+
write(entry: LogEntry): void {
|
|
29
|
+
// Deep-clone via `structuredClone` so nested objects in `data`
|
|
30
|
+
// are isolated — a shallow `{ ...entry.data }` would leak
|
|
31
|
+
// mutations of nested fields back into the captured entry.
|
|
32
|
+
this.#captured.push({
|
|
33
|
+
...entry,
|
|
34
|
+
data: entry.data ? deepClone(entry.data) : undefined,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Defensive snapshot of every captured entry. */
|
|
39
|
+
getLogged(): LogEntry[] {
|
|
40
|
+
return this.#captured.map((e) => ({
|
|
41
|
+
...e,
|
|
42
|
+
data: e.data ? deepClone(e.data) : undefined,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
reset(): void {
|
|
47
|
+
this.#captured = [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
assertLogged(level: LogLevel, predicate?: FakeLoggerPredicateArg): void {
|
|
51
|
+
const match = makeMatcher(level, predicate);
|
|
52
|
+
if (this.#captured.some(match)) return;
|
|
53
|
+
throw new Error(
|
|
54
|
+
`logger.assertLogged('${level}'${describePredicate(predicate)}) failed — no captured entry matches.\n${describeCaptured(this.#captured)}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
assertNotLogged(level: LogLevel, predicate?: FakeLoggerPredicateArg): void {
|
|
59
|
+
const match = makeMatcher(level, predicate);
|
|
60
|
+
if (!this.#captured.some(match)) return;
|
|
61
|
+
throw new Error(
|
|
62
|
+
`logger.assertNotLogged('${level}'${describePredicate(predicate)}) failed — at least one captured entry matches.\n${describeCaptured(this.#captured)}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function makeMatcher(
|
|
68
|
+
level: LogLevel,
|
|
69
|
+
predicate: FakeLoggerPredicateArg | undefined,
|
|
70
|
+
): (e: LogEntry) => boolean {
|
|
71
|
+
if (typeof predicate === "function") {
|
|
72
|
+
return (e) => e.level === level && predicate(e);
|
|
73
|
+
}
|
|
74
|
+
if (predicate === undefined) {
|
|
75
|
+
return (e) => e.level === level;
|
|
76
|
+
}
|
|
77
|
+
// Validate the predicate AT CONSTRUCTION, not inside the closure —
|
|
78
|
+
// Array.some short-circuits on an empty array, so a check inside
|
|
79
|
+
// the matcher would silently pass when nothing was captured.
|
|
80
|
+
if (predicate.containing === "") {
|
|
81
|
+
throw new Error(
|
|
82
|
+
"FakeLogger: `containing` predicate cannot be an empty string — it would match every captured message.",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return (e) => {
|
|
86
|
+
if (e.level !== level) return false;
|
|
87
|
+
if (
|
|
88
|
+
predicate.containing !== undefined &&
|
|
89
|
+
!e.message.includes(predicate.containing)
|
|
90
|
+
) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (predicate.dataMatches && !predicate.dataMatches(e.data)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function describePredicate(
|
|
101
|
+
predicate: FakeLoggerPredicateArg | undefined,
|
|
102
|
+
): string {
|
|
103
|
+
if (predicate === undefined) return "";
|
|
104
|
+
if (typeof predicate === "function") return ", <function predicate>";
|
|
105
|
+
if (Object.keys(predicate).length === 0)
|
|
106
|
+
return ", <empty predicate (level-only)>";
|
|
107
|
+
return `, ${safeStringify(predicate)}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function describeCaptured(captured: LogEntry[]): string {
|
|
111
|
+
if (captured.length === 0) return "Captured: (none)";
|
|
112
|
+
const lines = captured.map(
|
|
113
|
+
(e, i) => ` [${i}] ${e.level} module="${e.module}" message="${e.message}"`,
|
|
114
|
+
);
|
|
115
|
+
return `Captured (${captured.length}):\n${lines.join("\n")}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `JSON.stringify` with circular-ref + function-field handling.
|
|
119
|
+
* Functions render as `<function>`, circular refs as `<circular>`,
|
|
120
|
+
* unstringifiable values as `<unstringifiable>` — so an assertion
|
|
121
|
+
* failure message never gets eaten by a JSON throw. */
|
|
122
|
+
function safeStringify(value: unknown): string {
|
|
123
|
+
const seen = new WeakSet<object>();
|
|
124
|
+
try {
|
|
125
|
+
return JSON.stringify(value, (_key, v: unknown) => {
|
|
126
|
+
if (typeof v === "function") return "<function>";
|
|
127
|
+
if (typeof v === "object" && v !== null) {
|
|
128
|
+
if (seen.has(v)) return "<circular>";
|
|
129
|
+
seen.add(v);
|
|
130
|
+
}
|
|
131
|
+
return v;
|
|
132
|
+
});
|
|
133
|
+
} catch {
|
|
134
|
+
return "<unstringifiable>";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function deepClone<T>(value: T): T {
|
|
139
|
+
return structuredClone(value);
|
|
140
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spectrum types.
|
|
3
|
+
* @implements FR54, FR57
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
|
7
|
+
|
|
8
|
+
export const LOG_LEVEL_ORDER: Record<LogLevel, number> = {
|
|
9
|
+
trace: 0,
|
|
10
|
+
debug: 1,
|
|
11
|
+
info: 2,
|
|
12
|
+
warn: 3,
|
|
13
|
+
error: 4,
|
|
14
|
+
fatal: 5,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface LogEntry {
|
|
18
|
+
level: LogLevel;
|
|
19
|
+
message: string;
|
|
20
|
+
module: string;
|
|
21
|
+
correlationId?: string;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
data?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LogChannel {
|
|
27
|
+
name: string;
|
|
28
|
+
write(entry: LogEntry): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface LogConfig {
|
|
32
|
+
level: LogLevel;
|
|
33
|
+
channels: LogChannel[];
|
|
34
|
+
/** Per-module log level overrides */
|
|
35
|
+
modules?: Record<string, LogLevel>;
|
|
36
|
+
}
|