@ashlr/stack-core 0.1.1 → 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.
- package/LICENSE +1 -1
- package/README.md +26 -15
- package/package.json +1 -1
- package/src/__tests__/_harness.ts +67 -7
- package/src/__tests__/agent-onboard.test.ts +279 -0
- package/src/__tests__/api-key-providers.test.ts +6 -2
- package/src/__tests__/base-urls.test.ts +208 -0
- package/src/__tests__/catalog-index.test.ts +14 -20
- package/src/__tests__/catalog.test.ts +31 -0
- package/src/__tests__/cloudflare-provider.test.ts +32 -10
- package/src/__tests__/completion-ci.test.ts +239 -23
- package/src/__tests__/config-own-services.test.ts +183 -0
- package/src/__tests__/config-split.test.ts +272 -2
- package/src/__tests__/config.test.ts +94 -4
- package/src/__tests__/credential-staging.test.ts +660 -0
- package/src/__tests__/deps.test.ts +2 -1
- package/src/__tests__/detect-source.test.ts +183 -4
- package/src/__tests__/fixtures/stack-manifest.v1.json +10 -0
- package/src/__tests__/github-provider.test.ts +3 -2
- package/src/__tests__/helpers.test.ts +24 -4
- package/src/__tests__/http.test.ts +2 -2
- package/src/__tests__/import-integration.test.ts +18 -4
- package/src/__tests__/inference.test.ts +57 -13
- package/src/__tests__/interactive-prompt.test.ts +106 -0
- package/src/__tests__/mcp-mirror.test.ts +116 -0
- package/src/__tests__/neon-provider.test.ts +136 -3
- package/src/__tests__/new-providers.test.ts +470 -0
- package/src/__tests__/no-orphan-modules.test.ts +114 -0
- package/src/__tests__/oauth-port.test.ts +84 -0
- package/src/__tests__/openai-provider.test.ts +1 -1
- package/src/__tests__/phantom-contract.test.ts +177 -0
- package/src/__tests__/phantom-harness.test.ts +226 -8
- package/src/__tests__/phantom-process.test.ts +135 -0
- package/src/__tests__/phantom-project-preflight.test.ts +118 -0
- package/src/__tests__/phantom-wire.test.ts +403 -22
- package/src/__tests__/pipeline-integration.test.ts +33 -1
- package/src/__tests__/pipeline-recovery.test.ts +29 -28
- package/src/__tests__/pipeline-rollback.test.ts +509 -0
- package/src/__tests__/pipeline.test.ts +3 -1
- package/src/__tests__/pm-detect.test.ts +113 -0
- package/src/__tests__/provider-deprovision.test.ts +269 -0
- package/src/__tests__/provider-parity.test.ts +46 -0
- package/src/__tests__/publish-script.test.ts +704 -0
- package/src/__tests__/recipe.test.ts +27 -2
- package/src/__tests__/recommend-apply-e2e.test.ts +29 -27
- package/src/__tests__/registry.test.ts +11 -2
- package/src/__tests__/release-artifacts.test.ts +55 -0
- package/src/__tests__/scan-missing-dir.test.ts +45 -0
- package/src/__tests__/secret-redaction.test.ts +10 -10
- package/src/__tests__/security.test.ts +9 -10
- package/src/__tests__/stack-manifest.test.ts +38 -0
- package/src/__tests__/supabase-provider.test.ts +6 -4
- package/src/__tests__/swap.test.ts +114 -0
- package/src/__tests__/telemetry.test.ts +354 -0
- package/src/__tests__/template-litellm-gateway.test.ts +70 -0
- package/src/__tests__/template-temporal-worker-phantom.test.ts +45 -0
- package/src/__tests__/templates.test.ts +1 -1
- package/src/__tests__/turso-provider.test.ts +2 -5
- package/src/agent-onboard.ts +346 -0
- package/src/ai/catalog-index.ts +68 -10
- package/src/ai/circuit-breaker.ts +3 -6
- package/src/ai/cost-tracker.ts +6 -15
- package/src/ai/inference.ts +19 -11
- package/src/ai/phantom-wire.ts +73 -58
- package/src/ai/recipe.ts +69 -0
- package/src/base-urls.ts +71 -0
- package/src/catalog.ts +281 -2
- package/src/config.ts +31 -18
- package/src/detect-source.ts +50 -9
- package/src/detect.ts +13 -5
- package/src/errors.ts +6 -0
- package/src/http.ts +6 -5
- package/src/index.ts +16 -0
- package/src/mcp-writer.ts +97 -2
- package/src/oauth.ts +85 -53
- package/src/phantom-process.ts +72 -0
- package/src/phantom.ts +319 -30
- package/src/pipeline.ts +177 -68
- package/src/pm-detect.ts +56 -0
- package/src/providers/_api-key.ts +18 -17
- package/src/providers/_base.ts +28 -1
- package/src/providers/_credential-stage.ts +237 -0
- package/src/providers/_helpers.ts +55 -9
- package/src/providers/anthropic.ts +2 -1
- package/src/providers/auth0.ts +27 -0
- package/src/providers/aws.ts +32 -33
- package/src/providers/cloudflare.ts +18 -37
- package/src/providers/convex.ts +1 -2
- package/src/providers/datadog.ts +29 -0
- package/src/providers/digitalocean.ts +30 -0
- package/src/providers/gcp.ts +36 -0
- package/src/providers/github.ts +37 -35
- package/src/providers/grafana.ts +29 -0
- package/src/providers/hetzner.ts +29 -0
- package/src/providers/index.ts +13 -1
- package/src/providers/launchdarkly.ts +33 -0
- package/src/providers/mixpanel.ts +37 -0
- package/src/providers/neon.ts +50 -26
- package/src/providers/openai.ts +2 -1
- package/src/providers/plausible.ts +26 -0
- package/src/providers/sentry.ts +17 -17
- package/src/providers/stripe.ts +1 -2
- package/src/providers/supabase.ts +66 -37
- package/src/providers/turso.ts +49 -38
- package/src/providers/vercel.ts +50 -20
- package/src/providers/workos.ts +27 -0
- package/src/registry.ts +7 -11
- package/src/stack-manifest.ts +39 -0
- package/src/swap.ts +108 -0
- package/src/telemetry.ts +267 -0
- package/src/templates.ts +6 -2
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry module tests.
|
|
3
|
+
*
|
|
4
|
+
* Strategy: use __setConfigDirForTesting() to redirect all config I/O to a
|
|
5
|
+
* temp directory, bypassing the real ~/.ashlr/stack/config.json entirely.
|
|
6
|
+
* Tests write/delete the temp config file directly between runs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
__setConfigDirForTesting,
|
|
14
|
+
disable as disableTelemetry,
|
|
15
|
+
emit,
|
|
16
|
+
enable as enableTelemetry,
|
|
17
|
+
isEnabled,
|
|
18
|
+
} from "../telemetry.ts";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Temp config dir wired before any test runs
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
const TMP_CONFIG_DIR = join(import.meta.dir, "__telemetry_tmp__", ".ashlr", "stack");
|
|
25
|
+
const TMP_CONFIG_PATH = join(TMP_CONFIG_DIR, "config.json");
|
|
26
|
+
|
|
27
|
+
async function writeCfg(cfg: object): Promise<void> {
|
|
28
|
+
await mkdir(TMP_CONFIG_DIR, { recursive: true });
|
|
29
|
+
await writeFile(TMP_CONFIG_PATH, JSON.stringify(cfg, null, 2), "utf-8");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function deleteCfg(): Promise<void> {
|
|
33
|
+
await rm(TMP_CONFIG_PATH, { force: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let realFetch: typeof fetch;
|
|
37
|
+
|
|
38
|
+
beforeAll(async () => {
|
|
39
|
+
await mkdir(TMP_CONFIG_DIR, { recursive: true });
|
|
40
|
+
__setConfigDirForTesting(TMP_CONFIG_DIR);
|
|
41
|
+
realFetch = globalThis.fetch;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterAll(async () => {
|
|
45
|
+
__setConfigDirForTesting(undefined);
|
|
46
|
+
globalThis.fetch = realFetch;
|
|
47
|
+
await rm(join(import.meta.dir, "__telemetry_tmp__"), { recursive: true, force: true });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
await deleteCfg();
|
|
52
|
+
globalThis.fetch = realFetch;
|
|
53
|
+
Reflect.deleteProperty(process.env, "STACK_TELEMETRY");
|
|
54
|
+
Reflect.deleteProperty(process.env, "STACK_TELEMETRY_ENDPOINT");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
globalThis.fetch = realFetch;
|
|
59
|
+
Reflect.deleteProperty(process.env, "STACK_TELEMETRY");
|
|
60
|
+
Reflect.deleteProperty(process.env, "STACK_TELEMETRY_ENDPOINT");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// 1. emit when disabled is a no-op (fetch never called)
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
describe("emit when disabled", () => {
|
|
68
|
+
test("fetch is never called when config.enabled = false", async () => {
|
|
69
|
+
await writeCfg({ enabled: false, installId: "test-id-disabled" });
|
|
70
|
+
|
|
71
|
+
let fetchCalled = false;
|
|
72
|
+
globalThis.fetch = (async () => {
|
|
73
|
+
fetchCalled = true;
|
|
74
|
+
return new Response("ok", { status: 200 });
|
|
75
|
+
}) as unknown as typeof fetch;
|
|
76
|
+
|
|
77
|
+
await emit({ type: "command", command: "add", exitCode: 0, durationMs: 100 });
|
|
78
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
79
|
+
expect(fetchCalled).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("fetch is never called when no config file exists", async () => {
|
|
83
|
+
let fetchCalled = false;
|
|
84
|
+
globalThis.fetch = (async () => {
|
|
85
|
+
fetchCalled = true;
|
|
86
|
+
return new Response("ok", { status: 200 });
|
|
87
|
+
}) as unknown as typeof fetch;
|
|
88
|
+
|
|
89
|
+
await emit({ type: "command", command: "scan" });
|
|
90
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
91
|
+
expect(fetchCalled).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// 2. emit when enabled POSTs with expected payload
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
describe("emit when enabled", () => {
|
|
100
|
+
test("POSTs JSON with correct shape when STACK_TELEMETRY_ENDPOINT is set", async () => {
|
|
101
|
+
await writeCfg({ enabled: true, installId: "fixed-install-id" });
|
|
102
|
+
process.env.STACK_TELEMETRY_ENDPOINT = "https://example.com/v1/events";
|
|
103
|
+
|
|
104
|
+
let capturedBody: unknown;
|
|
105
|
+
let capturedUrl: string | undefined;
|
|
106
|
+
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
107
|
+
capturedUrl = url as string;
|
|
108
|
+
capturedBody = JSON.parse(init?.body as string);
|
|
109
|
+
return new Response("ok", { status: 200 });
|
|
110
|
+
}) as unknown as typeof fetch;
|
|
111
|
+
|
|
112
|
+
await emit({ type: "command", command: "add", exitCode: 0, durationMs: 42 });
|
|
113
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
114
|
+
|
|
115
|
+
expect(capturedUrl).toBe("https://example.com/v1/events");
|
|
116
|
+
const body = capturedBody as Record<string, unknown>;
|
|
117
|
+
expect(body.type).toBe("command");
|
|
118
|
+
expect(body.command).toBe("add");
|
|
119
|
+
expect(body.exitCode).toBe(0);
|
|
120
|
+
expect(body.durationMs).toBe(42);
|
|
121
|
+
expect(body.installId).toBe("fixed-install-id");
|
|
122
|
+
expect(typeof body.runId).toBe("string");
|
|
123
|
+
expect(body.platform).toBeDefined();
|
|
124
|
+
expect(body.stackVersion).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("fetch is never called when enabled but no endpoint configured", async () => {
|
|
128
|
+
await writeCfg({ enabled: true, installId: "no-endpoint-id" });
|
|
129
|
+
// Neither STACK_TELEMETRY_ENDPOINT nor config.endpoint is set.
|
|
130
|
+
|
|
131
|
+
let fetchCalled = false;
|
|
132
|
+
globalThis.fetch = (async () => {
|
|
133
|
+
fetchCalled = true;
|
|
134
|
+
return new Response("ok", { status: 200 });
|
|
135
|
+
}) as unknown as typeof fetch;
|
|
136
|
+
|
|
137
|
+
await emit({ type: "command", command: "add", exitCode: 0, durationMs: 10 });
|
|
138
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
139
|
+
expect(fetchCalled).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("config.endpoint is used when STACK_TELEMETRY_ENDPOINT is not set", async () => {
|
|
143
|
+
await writeCfg({
|
|
144
|
+
enabled: true,
|
|
145
|
+
installId: "cfg-endpoint-id",
|
|
146
|
+
endpoint: "https://self-hosted.example.com/events",
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
let capturedUrl: string | undefined;
|
|
150
|
+
globalThis.fetch = (async (url: string) => {
|
|
151
|
+
capturedUrl = url as string;
|
|
152
|
+
return new Response("ok", { status: 200 });
|
|
153
|
+
}) as unknown as typeof fetch;
|
|
154
|
+
|
|
155
|
+
await emit({ type: "command", command: "scan" });
|
|
156
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
157
|
+
expect(capturedUrl).toBe("https://self-hosted.example.com/events");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// 3. STACK_TELEMETRY=0 overrides to disabled
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
describe("STACK_TELEMETRY env var", () => {
|
|
166
|
+
test("STACK_TELEMETRY=0 prevents emit even when config.enabled = true", async () => {
|
|
167
|
+
await writeCfg({ enabled: true, installId: "env-off-test" });
|
|
168
|
+
process.env.STACK_TELEMETRY = "0";
|
|
169
|
+
|
|
170
|
+
let fetchCalled = false;
|
|
171
|
+
globalThis.fetch = (async () => {
|
|
172
|
+
fetchCalled = true;
|
|
173
|
+
return new Response("ok", { status: 200 });
|
|
174
|
+
}) as unknown as typeof fetch;
|
|
175
|
+
|
|
176
|
+
await emit({ type: "command", command: "scan" });
|
|
177
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
178
|
+
expect(fetchCalled).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// 4. STACK_TELEMETRY=1 without prior opt-in stays disabled
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
test("STACK_TELEMETRY=1 alone does not enable telemetry (no prior opt-in)", async () => {
|
|
186
|
+
await writeCfg({ enabled: false, installId: "env-on-test" });
|
|
187
|
+
process.env.STACK_TELEMETRY = "1";
|
|
188
|
+
|
|
189
|
+
let fetchCalled = false;
|
|
190
|
+
globalThis.fetch = (async () => {
|
|
191
|
+
fetchCalled = true;
|
|
192
|
+
return new Response("ok", { status: 200 });
|
|
193
|
+
}) as unknown as typeof fetch;
|
|
194
|
+
|
|
195
|
+
// isEnabled() must return false — env=1 is not a substitute for opt-in.
|
|
196
|
+
expect(await isEnabled()).toBe(false);
|
|
197
|
+
|
|
198
|
+
await emit({ type: "command", command: "add" });
|
|
199
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
200
|
+
expect(fetchCalled).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// 5. emit with fetch throwing → no error propagated
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
describe("emit error swallowing", () => {
|
|
209
|
+
test("network error does not propagate to the caller", async () => {
|
|
210
|
+
await writeCfg({ enabled: true, installId: "throw-test-id" });
|
|
211
|
+
process.env.STACK_TELEMETRY_ENDPOINT = "https://example.com/v1/events";
|
|
212
|
+
globalThis.fetch = (async () => {
|
|
213
|
+
throw new Error("network failure");
|
|
214
|
+
}) as unknown as typeof fetch;
|
|
215
|
+
|
|
216
|
+
await expect(
|
|
217
|
+
emit({ type: "error", command: "add", exitCode: 1, durationMs: 5 }),
|
|
218
|
+
).resolves.toBeUndefined();
|
|
219
|
+
|
|
220
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// 6. installId persists across calls
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
describe("installId persistence", () => {
|
|
229
|
+
test("same installId is emitted on every call", async () => {
|
|
230
|
+
await writeCfg({ enabled: true, installId: "persisted-id-123" });
|
|
231
|
+
process.env.STACK_TELEMETRY_ENDPOINT = "https://example.com/v1/events";
|
|
232
|
+
|
|
233
|
+
const ids: string[] = [];
|
|
234
|
+
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
|
|
235
|
+
const body = JSON.parse(init?.body as string) as Record<string, unknown>;
|
|
236
|
+
ids.push(body.installId as string);
|
|
237
|
+
return new Response("ok", { status: 200 });
|
|
238
|
+
}) as unknown as typeof fetch;
|
|
239
|
+
|
|
240
|
+
await emit({ type: "command", command: "add" });
|
|
241
|
+
await emit({ type: "command", command: "scan" });
|
|
242
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
243
|
+
|
|
244
|
+
expect(ids.length).toBe(2);
|
|
245
|
+
expect(ids[0]).toBe("persisted-id-123");
|
|
246
|
+
expect(ids[1]).toBe("persisted-id-123");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// 7. No path/cwd/project in the emitted payload
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
describe("privacy: banned fields absent from payload", () => {
|
|
255
|
+
const BANNED_KEYS = [
|
|
256
|
+
"cwd",
|
|
257
|
+
"projectName",
|
|
258
|
+
"project_name",
|
|
259
|
+
"project",
|
|
260
|
+
"provider",
|
|
261
|
+
"providerName",
|
|
262
|
+
"secret",
|
|
263
|
+
"secretValue",
|
|
264
|
+
"secretKey",
|
|
265
|
+
"envValue",
|
|
266
|
+
"homePath",
|
|
267
|
+
"home",
|
|
268
|
+
"stackToml",
|
|
269
|
+
"stack_toml",
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
test("emitted payload contains none of the banned keys", async () => {
|
|
273
|
+
await writeCfg({ enabled: true, installId: "privacy-test-id" });
|
|
274
|
+
process.env.STACK_TELEMETRY_ENDPOINT = "https://example.com/v1/events";
|
|
275
|
+
|
|
276
|
+
let capturedBody: Record<string, unknown> = {};
|
|
277
|
+
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
|
|
278
|
+
capturedBody = JSON.parse(init?.body as string) as Record<string, unknown>;
|
|
279
|
+
return new Response("ok", { status: 200 });
|
|
280
|
+
}) as unknown as typeof fetch;
|
|
281
|
+
|
|
282
|
+
await emit({ type: "command", command: "add", exitCode: 0, durationMs: 10 });
|
|
283
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
284
|
+
|
|
285
|
+
for (const key of BANNED_KEYS) {
|
|
286
|
+
expect(Object.prototype.hasOwnProperty.call(capturedBody, key)).toBe(false);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("installId is a UUID (no path segments)", async () => {
|
|
291
|
+
await writeCfg({ enabled: true, installId: crypto.randomUUID() });
|
|
292
|
+
process.env.STACK_TELEMETRY_ENDPOINT = "https://example.com/v1/events";
|
|
293
|
+
|
|
294
|
+
let capturedBody: Record<string, unknown> = {};
|
|
295
|
+
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
|
|
296
|
+
capturedBody = JSON.parse(init?.body as string) as Record<string, unknown>;
|
|
297
|
+
return new Response("ok", { status: 200 });
|
|
298
|
+
}) as unknown as typeof fetch;
|
|
299
|
+
|
|
300
|
+
await emit({ type: "command", command: "scan" });
|
|
301
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
302
|
+
|
|
303
|
+
const installId = capturedBody.installId as string;
|
|
304
|
+
expect(installId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
|
305
|
+
expect(installId).not.toContain("/");
|
|
306
|
+
expect(installId).not.toContain("\\");
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// 8. isEnabled reflects stored config
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
describe("isEnabled", () => {
|
|
315
|
+
test("returns false when no config file exists", async () => {
|
|
316
|
+
expect(await isEnabled()).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("returns true when config.enabled = true", async () => {
|
|
320
|
+
await writeCfg({ enabled: true, installId: "check-id" });
|
|
321
|
+
expect(await isEnabled()).toBe(true);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("returns false when STACK_TELEMETRY=0 even if enabled=true", async () => {
|
|
325
|
+
await writeCfg({ enabled: true, installId: "check-id" });
|
|
326
|
+
process.env.STACK_TELEMETRY = "0";
|
|
327
|
+
expect(await isEnabled()).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// 9. disable() and enable() round-trip
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
describe("disable / enable", () => {
|
|
336
|
+
test("disable() writes enabled=false", async () => {
|
|
337
|
+
await writeCfg({ enabled: true, installId: "round-trip-id" });
|
|
338
|
+
await disableTelemetry();
|
|
339
|
+
expect(await isEnabled()).toBe(false);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("enable() writes enabled=true and creates installId if missing", async () => {
|
|
343
|
+
await writeCfg({ enabled: false });
|
|
344
|
+
await enableTelemetry();
|
|
345
|
+
expect(await isEnabled()).toBe(true);
|
|
346
|
+
|
|
347
|
+
const raw = await readFile(TMP_CONFIG_PATH, "utf-8");
|
|
348
|
+
const cfg = JSON.parse(raw) as { installId?: string };
|
|
349
|
+
expect(cfg.installId).toBeDefined();
|
|
350
|
+
expect(cfg.installId).toMatch(
|
|
351
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { PROVIDERS_REF } from "../catalog.ts";
|
|
5
|
+
import { loadTemplate } from "../templates.ts";
|
|
6
|
+
|
|
7
|
+
const dir = join(import.meta.dir, "..", "..", "..", "..", "templates", "litellm-gateway");
|
|
8
|
+
const readme = readFileSync(join(dir, "README.md"), "utf-8");
|
|
9
|
+
|
|
10
|
+
describe("litellm-gateway template", () => {
|
|
11
|
+
test("declares only catalog providers and their own secret slots, no values", async () => {
|
|
12
|
+
const cfg = await loadTemplate("litellm-gateway");
|
|
13
|
+
expect(cfg).toBeDefined();
|
|
14
|
+
const services = Object.entries(cfg?.services ?? {});
|
|
15
|
+
expect(services.length).toBeGreaterThan(0);
|
|
16
|
+
for (const [name, svc] of services) {
|
|
17
|
+
const ref = PROVIDERS_REF.find((p) => p.name === svc.provider);
|
|
18
|
+
expect(ref, `${name} uses a provider in the catalog`).toBeDefined();
|
|
19
|
+
for (const secret of svc.secrets ?? []) expect(ref?.secrets).toContain(secret);
|
|
20
|
+
}
|
|
21
|
+
const raw = readFileSync(join(dir, "stack.toml"), "utf-8");
|
|
22
|
+
expect(raw).not.toMatch(/sk-[A-Za-z0-9]/);
|
|
23
|
+
expect(raw).toContain('template = "litellm-gateway"');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("README keeps the pin and isolation guidance", () => {
|
|
27
|
+
for (const phrase of [
|
|
28
|
+
"Pin, never float",
|
|
29
|
+
"GHSA-5mg7-485q-xm76",
|
|
30
|
+
"no mounted dotfiles",
|
|
31
|
+
"Bind to loopback",
|
|
32
|
+
"Placeholders only",
|
|
33
|
+
"allow_client_side_credentials: false",
|
|
34
|
+
"NO_DOCS",
|
|
35
|
+
"Cursor is not supported",
|
|
36
|
+
]) {
|
|
37
|
+
expect(readme, phrase).toContain(phrase);
|
|
38
|
+
}
|
|
39
|
+
expect(readme).not.toContain("\u2014");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("README prerequisites match the template and distinguish local checks from integration", async () => {
|
|
43
|
+
const cfg = await loadTemplate("litellm-gateway");
|
|
44
|
+
expect(cfg).toBeDefined();
|
|
45
|
+
const table = readme.match(
|
|
46
|
+
/\| Service \| Phantom secret name \|\n\| --- \| --- \|\n((?:\| `[^`]+` \| `[^`]+` \|\n)+)/,
|
|
47
|
+
);
|
|
48
|
+
expect(table).not.toBeNull();
|
|
49
|
+
const documentedSlots = [...(table?.[1] ?? "").matchAll(/\| `([^`]+)` \| `([^`]+)` \|/g)].map(
|
|
50
|
+
([, service, secret]) => [service, secret],
|
|
51
|
+
);
|
|
52
|
+
const configuredSlots = Object.entries(cfg?.services ?? {}).flatMap(([service, entry]) =>
|
|
53
|
+
entry.secrets.map((secret) => [service, secret]),
|
|
54
|
+
);
|
|
55
|
+
expect(documentedSlots).toEqual(configuredSlots);
|
|
56
|
+
expect(cfg?.environments).toEqual([{ name: "dev", default: true }]);
|
|
57
|
+
expect(configuredSlots.flat()).not.toContain("LITELLM_MASTER_KEY");
|
|
58
|
+
for (const phrase of [
|
|
59
|
+
"stack init --template litellm-gateway --noProvision",
|
|
60
|
+
"service is already in the stack",
|
|
61
|
+
"does not set it",
|
|
62
|
+
"stack templates list --json",
|
|
63
|
+
"template-litellm-gateway.test.ts",
|
|
64
|
+
"local mock upstream and dummy credentials",
|
|
65
|
+
"does not establish those properties",
|
|
66
|
+
]) {
|
|
67
|
+
expect(readme, phrase).toContain(phrase);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { loadTemplate } from "../templates.ts";
|
|
5
|
+
|
|
6
|
+
const dir = join(import.meta.dir, "..", "..", "..", "..", "templates", "temporal-worker-phantom");
|
|
7
|
+
const readme = readFileSync(join(dir, "README.md"), "utf-8");
|
|
8
|
+
|
|
9
|
+
describe("temporal-worker-phantom template", () => {
|
|
10
|
+
test("loads with no services and holds no secret values", async () => {
|
|
11
|
+
const cfg = await loadTemplate("temporal-worker-phantom");
|
|
12
|
+
expect(cfg).toBeDefined();
|
|
13
|
+
expect(cfg?.services).toEqual({});
|
|
14
|
+
expect(cfg?.environments).toEqual([{ name: "dev", default: true }]);
|
|
15
|
+
const raw = readFileSync(join(dir, "stack.toml"), "utf-8");
|
|
16
|
+
expect(raw).toContain('template = "temporal-worker-phantom"');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("README describes the configuration and limits of local verification", () => {
|
|
20
|
+
for (const phrase of [
|
|
21
|
+
"stack init --template temporal-worker-phantom",
|
|
22
|
+
"stack list --json",
|
|
23
|
+
'"services": []',
|
|
24
|
+
"default `dev` environment",
|
|
25
|
+
"not connectivity, credentials or worker execution",
|
|
26
|
+
"does not guarantee raw credentials",
|
|
27
|
+
]) {
|
|
28
|
+
expect(readme, phrase).toContain(phrase);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("README keeps the security rules and avoids em dashes", () => {
|
|
33
|
+
for (const phrase of [
|
|
34
|
+
"phantom exec",
|
|
35
|
+
"No secret values in workflow data",
|
|
36
|
+
"Codec Server stays local",
|
|
37
|
+
"noopAuthorizer",
|
|
38
|
+
"start-dev",
|
|
39
|
+
"Keep Temporal optional",
|
|
40
|
+
]) {
|
|
41
|
+
expect(readme, phrase).toContain(phrase);
|
|
42
|
+
}
|
|
43
|
+
expect(readme).not.toContain("\u2014");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { listTemplates, loadTemplate } from "../templates.ts";
|
|
3
2
|
import { listProviderNames } from "../providers/index.ts";
|
|
3
|
+
import { listTemplates, loadTemplate } from "../templates.ts";
|
|
4
4
|
|
|
5
5
|
describe("starter templates", () => {
|
|
6
6
|
test("all templates reference only registered providers", async () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import turso from "../providers/turso.ts";
|
|
3
2
|
import type { ProviderContext } from "../providers/_base.ts";
|
|
3
|
+
import turso from "../providers/turso.ts";
|
|
4
4
|
import { type Harness, setupFakePhantom } from "./_harness.ts";
|
|
5
5
|
|
|
6
6
|
describe("turso provider (mocked fetch)", () => {
|
|
@@ -22,10 +22,7 @@ describe("turso provider (mocked fetch)", () => {
|
|
|
22
22
|
const u = String(url);
|
|
23
23
|
const method = init?.method ?? "GET";
|
|
24
24
|
if (u.endsWith("/organizations") && method === "GET") {
|
|
25
|
-
return new Response(
|
|
26
|
-
JSON.stringify([{ slug: "my-org", name: "My Org" }]),
|
|
27
|
-
{ status: 200 },
|
|
28
|
-
);
|
|
25
|
+
return new Response(JSON.stringify([{ slug: "my-org", name: "My Org" }]), { status: 200 });
|
|
29
26
|
}
|
|
30
27
|
if (u.endsWith("/organizations/my-org/databases") && method === "POST") {
|
|
31
28
|
return new Response(
|