kubeagent 0.1.43 → 0.1.45

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 (77) hide show
  1. package/package.json +5 -2
  2. package/dist/auth.d.ts +0 -29
  3. package/dist/check/index.d.ts +0 -34
  4. package/dist/check/index.test.d.ts +0 -1
  5. package/dist/check/index.test.js +0 -202
  6. package/dist/check/run.d.ts +0 -10
  7. package/dist/cli.d.ts +0 -2
  8. package/dist/config.d.ts +0 -75
  9. package/dist/debug.d.ts +0 -10
  10. package/dist/detector/cache.d.ts +0 -6
  11. package/dist/detector/cache.test.d.ts +0 -1
  12. package/dist/detector/cache.test.js +0 -54
  13. package/dist/detector/catalog.d.ts +0 -17
  14. package/dist/detector/catalog.test.d.ts +0 -1
  15. package/dist/detector/catalog.test.js +0 -33
  16. package/dist/detector/eol.d.ts +0 -13
  17. package/dist/detector/eol.test.d.ts +0 -1
  18. package/dist/detector/eol.test.js +0 -78
  19. package/dist/detector/image-parser.d.ts +0 -11
  20. package/dist/detector/image-parser.test.d.ts +0 -1
  21. package/dist/detector/image-parser.test.js +0 -87
  22. package/dist/detector/index.d.ts +0 -33
  23. package/dist/detector/index.test.d.ts +0 -1
  24. package/dist/detector/index.test.js +0 -210
  25. package/dist/detector/osv.d.ts +0 -12
  26. package/dist/detector/osv.test.d.ts +0 -1
  27. package/dist/detector/osv.test.js +0 -86
  28. package/dist/detector/report.d.ts +0 -6
  29. package/dist/detector/sources.d.ts +0 -55
  30. package/dist/detector/sources.test.d.ts +0 -1
  31. package/dist/detector/sources.test.js +0 -36
  32. package/dist/diagnoser/approval.test.d.ts +0 -1
  33. package/dist/diagnoser/approval.test.js +0 -49
  34. package/dist/diagnoser/index.d.ts +0 -33
  35. package/dist/diagnoser/tools.d.ts +0 -119
  36. package/dist/kb/loader.d.ts +0 -1
  37. package/dist/kb/writer.d.ts +0 -12
  38. package/dist/kubectl-config.d.ts +0 -7
  39. package/dist/kubectl.d.ts +0 -13
  40. package/dist/monitor/checks.d.ts +0 -71
  41. package/dist/monitor/index.d.ts +0 -27
  42. package/dist/monitor/resolve.test.d.ts +0 -1
  43. package/dist/monitor/resolve.test.js +0 -21
  44. package/dist/monitor/scope.test.d.ts +0 -1
  45. package/dist/monitor/scope.test.js +0 -87
  46. package/dist/monitor/types.d.ts +0 -11
  47. package/dist/notify/discord.d.ts +0 -4
  48. package/dist/notify/index.d.ts +0 -6
  49. package/dist/notify/pagerduty.d.ts +0 -9
  50. package/dist/notify/pagerduty.test.d.ts +0 -1
  51. package/dist/notify/pagerduty.test.js +0 -97
  52. package/dist/notify/setup.d.ts +0 -9
  53. package/dist/notify/slack.d.ts +0 -4
  54. package/dist/notify/ssrf-guard.d.ts +0 -5
  55. package/dist/notify/teams.d.ts +0 -3
  56. package/dist/notify/telegram.d.ts +0 -9
  57. package/dist/notify/webhook.d.ts +0 -3
  58. package/dist/notify/webhook.test.d.ts +0 -1
  59. package/dist/notify/webhook.test.js +0 -74
  60. package/dist/onboard/cluster-scan.d.ts +0 -42
  61. package/dist/onboard/code-scan.d.ts +0 -9
  62. package/dist/onboard/index.d.ts +0 -3
  63. package/dist/onboard/interview.d.ts +0 -12
  64. package/dist/onboard/project-matcher.d.ts +0 -25
  65. package/dist/orchestrator.d.ts +0 -3
  66. package/dist/proxy-client.d.ts +0 -27
  67. package/dist/render.d.ts +0 -5
  68. package/dist/telemetry.d.ts +0 -1
  69. package/dist/telemetry.test.d.ts +0 -1
  70. package/dist/telemetry.test.js +0 -18
  71. package/dist/update-notifier.d.ts +0 -1
  72. package/dist/update-notifier.test.d.ts +0 -1
  73. package/dist/update-notifier.test.js +0 -50
  74. package/dist/verifier.d.ts +0 -9
  75. package/dist/version-check.d.ts +0 -14
  76. package/dist/version-check.test.d.ts +0 -1
  77. package/dist/version-check.test.js +0 -179
@@ -1,42 +0,0 @@
1
- import { type KubectlOptions } from "../kubectl.js";
2
- export interface NodeInfo {
3
- name: string;
4
- roles: string[];
5
- version: string;
6
- os: string;
7
- }
8
- export interface DeploymentInfo {
9
- name: string;
10
- namespace: string;
11
- replicas: number;
12
- image: string;
13
- }
14
- export interface StatefulSetInfo {
15
- name: string;
16
- namespace: string;
17
- replicas: number;
18
- image: string;
19
- }
20
- export interface ServiceInfo {
21
- name: string;
22
- namespace: string;
23
- type: string;
24
- ports: string;
25
- }
26
- export interface IngressInfo {
27
- name: string;
28
- namespace: string;
29
- hosts: string[];
30
- tls: boolean;
31
- }
32
- export interface ClusterInfo {
33
- context: string;
34
- nodes: NodeInfo[];
35
- namespaces: string[];
36
- deployments: DeploymentInfo[];
37
- statefulsets: StatefulSetInfo[];
38
- services: ServiceInfo[];
39
- ingresses: IngressInfo[];
40
- }
41
- export declare function scanCluster(options: KubectlOptions, onProgress?: (step: string) => void): Promise<ClusterInfo>;
42
- export declare function formatClusterMarkdown(info: ClusterInfo): string;
@@ -1,9 +0,0 @@
1
- export interface TechStack {
2
- language: string;
3
- framework: string;
4
- runtimeVersion: string;
5
- dependencies: string[];
6
- buildTool: string;
7
- }
8
- export declare function detectTechStack(codePath: string): TechStack;
9
- export declare function formatProjectMarkdown(projectName: string, codePath: string, stack: TechStack, extraNotes?: string): string;
@@ -1,3 +0,0 @@
1
- export declare function onboard(opts?: {
2
- timeout?: number;
3
- }): Promise<void>;
@@ -1,12 +0,0 @@
1
- import type { ClusterInfo } from "./cluster-scan.js";
2
- import type { TechStack } from "./code-scan.js";
3
- interface InterviewContext {
4
- cluster: ClusterInfo;
5
- projects: Array<{
6
- name: string;
7
- path: string;
8
- stack: TechStack;
9
- }>;
10
- }
11
- export declare function runInterview(context: InterviewContext): Promise<Map<string, string>>;
12
- export {};
@@ -1,25 +0,0 @@
1
- import { type TechStack } from "./code-scan.js";
2
- import type { DeploymentInfo, StatefulSetInfo } from "./cluster-scan.js";
3
- export interface ProjectCandidate {
4
- dir: string;
5
- name: string;
6
- stack: TechStack;
7
- packageName?: string;
8
- }
9
- export interface WorkloadRef {
10
- name: string;
11
- namespace: string;
12
- image: string;
13
- kind: "Deployment" | "StatefulSet";
14
- }
15
- export interface MatchSuggestion {
16
- project: ProjectCandidate;
17
- workload: WorkloadRef;
18
- score: number;
19
- reasons: string[];
20
- }
21
- /** Scan a parent directory and return all subdirectories that look like code projects. */
22
- export declare function scanProjectDirectory(parentDir: string): ProjectCandidate[];
23
- export declare function matchProjectsToWorkloads(projects: ProjectCandidate[], deployments: DeploymentInfo[], statefulsets: StatefulSetInfo[]): MatchSuggestion[];
24
- /** For each project, return only its best-matching workload (if score ≥ threshold). */
25
- export declare function bestMatches(suggestions: MatchSuggestion[], threshold?: number): MatchSuggestion[];
@@ -1,3 +0,0 @@
1
- import type { Issue } from "./monitor/types.js";
2
- import type { KubeAgentConfig } from "./config.js";
3
- export declare function handleIssues(issues: Issue[], config: KubeAgentConfig, clusterContext?: string, noInteractive?: boolean): Promise<void>;
@@ -1,27 +0,0 @@
1
- import type { AuthState } from "./auth.js";
2
- import type { V1MessagesRequest, V1MessagesResponse } from "@kubeagent/shared/v1-messages.js";
3
- export declare function proxyRequest(auth: AuthState, body: V1MessagesRequest): Promise<V1MessagesResponse>;
4
- export declare function fetchSlackWebhook(auth: AuthState): Promise<{
5
- connected: boolean;
6
- webhookUrl?: string;
7
- channelName?: string;
8
- teamName?: string;
9
- }>;
10
- export declare function reportIncident(auth: AuthState, incident: {
11
- clusterContext: string;
12
- issues: string;
13
- analysis: string;
14
- actionTaken?: string;
15
- verified?: boolean;
16
- }): Promise<void>;
17
- export declare function registerWebhook(auth: AuthState, url: string, label: string | undefined, severity: string): Promise<{
18
- secret?: string;
19
- }>;
20
- export declare function notifyViaServer(auth: AuthState, issues: Array<{
21
- kind: string;
22
- severity: string;
23
- message: string;
24
- namespace?: string;
25
- resource?: string;
26
- }>, clusterContext?: string): Promise<void>;
27
- export declare function pingWatchCycle(auth: AuthState): Promise<void>;
package/dist/render.d.ts DELETED
@@ -1,5 +0,0 @@
1
- export declare function renderMarkdown(text: string): string;
2
- /** Print a labelled section divider: ── Label ──────────── */
3
- export declare function sectionHeader(label: string, color?: import("chalk").ChalkInstance): void;
4
- /** Print a two-line command approval box. */
5
- export declare function commandBox(cmd: string): void;
@@ -1 +0,0 @@
1
- export declare function sendTelemetry(cliVersion: string): void;
@@ -1 +0,0 @@
1
- export {};
@@ -1,18 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { join } from "node:path";
3
- import { homedir } from "node:os";
4
- const TELEMETRY_DIR = join(homedir(), ".kubeagent");
5
- const SENT_FILE = join(TELEMETRY_DIR, ".telemetry-sent");
6
- describe("sendTelemetry", () => {
7
- it("module exports sendTelemetry function", async () => {
8
- const mod = await import("./telemetry.js");
9
- expect(typeof mod.sendTelemetry).toBe("function");
10
- });
11
- it("does not throw when called", async () => {
12
- // If marker file already exists from a real run, this just returns early.
13
- // If not, it fires a fetch that will fail silently (no server).
14
- // Either way it should not throw.
15
- const { sendTelemetry } = await import("./telemetry.js");
16
- expect(() => sendTelemetry("0.0.0-test")).not.toThrow();
17
- });
18
- });
@@ -1 +0,0 @@
1
- export declare function checkForUpdate(): Promise<void>;
@@ -1 +0,0 @@
1
- export {};
@@ -1,50 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- // Mock chalk to return plain strings
3
- vi.mock("chalk", () => {
4
- const identity = (s) => s;
5
- const handler = {
6
- get: () => new Proxy(identity, handler),
7
- apply: (_t, _this, args) => args[0],
8
- };
9
- return { default: new Proxy(identity, handler) };
10
- });
11
- describe("update-notifier", () => {
12
- let originalFetch;
13
- beforeEach(() => {
14
- originalFetch = globalThis.fetch;
15
- });
16
- afterEach(() => {
17
- globalThis.fetch = originalFetch;
18
- vi.restoreAllMocks();
19
- });
20
- it("prints update message when newer version exists", async () => {
21
- globalThis.fetch = vi.fn().mockResolvedValue({
22
- ok: true,
23
- json: () => Promise.resolve({ version: "99.0.0" }),
24
- });
25
- const spy = vi.spyOn(console, "log").mockImplementation(() => { });
26
- const { checkForUpdate } = await import("./update-notifier.js");
27
- await checkForUpdate();
28
- expect(spy).toHaveBeenCalled();
29
- const output = spy.mock.calls.map((c) => c.join(" ")).join("\n");
30
- expect(output).toContain("Update available");
31
- expect(output).toContain("99.0.0");
32
- });
33
- it("prints nothing when already up to date", async () => {
34
- globalThis.fetch = vi.fn().mockResolvedValue({
35
- ok: true,
36
- json: () => Promise.resolve({ version: "0.0.1" }),
37
- });
38
- const spy = vi.spyOn(console, "log").mockImplementation(() => { });
39
- const { checkForUpdate } = await import("./update-notifier.js");
40
- await checkForUpdate();
41
- expect(spy).not.toHaveBeenCalled();
42
- });
43
- it("prints nothing on network error", async () => {
44
- globalThis.fetch = vi.fn().mockRejectedValue(new Error("offline"));
45
- const spy = vi.spyOn(console, "log").mockImplementation(() => { });
46
- const { checkForUpdate } = await import("./update-notifier.js");
47
- await checkForUpdate();
48
- expect(spy).not.toHaveBeenCalled();
49
- });
50
- });
@@ -1,9 +0,0 @@
1
- import type { Issue, IssueKind } from "./monitor/types.js";
2
- import type { KubectlOptions } from "./kubectl.js";
3
- export interface VerificationResult {
4
- passed: boolean;
5
- remainingIssues: Issue[];
6
- message: string;
7
- }
8
- export declare function evaluateResult(currentIssues: Issue[], originalIssueKinds: IssueKind[]): VerificationResult;
9
- export declare function verify(originalIssueKinds: IssueKind[], kubectlOptions: KubectlOptions, cooldownMs?: number): Promise<VerificationResult>;
@@ -1,14 +0,0 @@
1
- export declare function isBelow(version: string, floor: string): boolean;
2
- /**
3
- * Verifies that the running CLI is still supported by the server at
4
- * `serverUrl`.
5
- *
6
- * Behaviour:
7
- * - Fetches the server's `minSupported` version (cached per-server for 24h).
8
- * - If the running version is strictly below the floor, prints a mandatory-
9
- * update message and exits the process with code 1.
10
- * - On network failure with no cached value, returns silently — we don't
11
- * want to lock users out when the server is unreachable for transient
12
- * reasons. A stale cache is preferred over no cache when offline.
13
- */
14
- export declare function enforceMinVersion(currentVersion: string, serverUrl: string): Promise<void>;
@@ -1 +0,0 @@
1
- export {};
@@ -1,179 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mkdtempSync, rmSync, writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- // Mock chalk to return plain strings
6
- vi.mock("chalk", () => {
7
- const identity = (s) => s;
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
- const handler = {
10
- get: () => new Proxy(identity, handler),
11
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
- apply: (_t, _this, args) => args[0],
13
- };
14
- return { default: new Proxy(identity, handler) };
15
- });
16
- let tmpHome;
17
- let originalHome;
18
- beforeEach(() => {
19
- tmpHome = mkdtempSync(join(tmpdir(), "kubeagent-version-test-"));
20
- originalHome = process.env.HOME;
21
- process.env.HOME = tmpHome;
22
- });
23
- afterEach(() => {
24
- if (originalHome !== undefined)
25
- process.env.HOME = originalHome;
26
- else
27
- delete process.env.HOME;
28
- rmSync(tmpHome, { recursive: true, force: true });
29
- vi.restoreAllMocks();
30
- });
31
- describe("isBelow", () => {
32
- it("returns true when current is older", async () => {
33
- const { isBelow } = await import("./version-check.js");
34
- expect(isBelow("0.1.5", "0.2.0")).toBe(true);
35
- expect(isBelow("0.1.5", "0.1.6")).toBe(true);
36
- expect(isBelow("0.0.9", "1.0.0")).toBe(true);
37
- });
38
- it("returns false when current is equal or newer", async () => {
39
- const { isBelow } = await import("./version-check.js");
40
- expect(isBelow("0.2.0", "0.2.0")).toBe(false);
41
- expect(isBelow("0.2.1", "0.2.0")).toBe(false);
42
- expect(isBelow("1.0.0", "0.9.99")).toBe(false);
43
- });
44
- it("strips a leading v", async () => {
45
- const { isBelow } = await import("./version-check.js");
46
- expect(isBelow("v0.1.0", "0.2.0")).toBe(true);
47
- expect(isBelow("0.1.0", "v0.2.0")).toBe(true);
48
- });
49
- });
50
- describe("enforceMinVersion", () => {
51
- it("exits with code 1 when current version is below min", async () => {
52
- globalThis.fetch = vi.fn().mockResolvedValue({
53
- ok: true,
54
- json: () => Promise.resolve({ minSupported: "0.5.0" }),
55
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
- });
57
- const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
58
- const exitSpy = vi.spyOn(process, "exit").mockImplementation(((_code) => {
59
- throw new Error("__exit__");
60
- }));
61
- const { enforceMinVersion } = await import("./version-check.js");
62
- await expect(enforceMinVersion("0.1.0", "https://api.example.com")).rejects.toThrow("__exit__");
63
- expect(exitSpy).toHaveBeenCalledWith(1);
64
- const output = errSpy.mock.calls.map((c) => c.join(" ")).join("\n");
65
- expect(output).toContain("no longer supported");
66
- expect(output).toContain("0.5.0");
67
- });
68
- it("does nothing when current version meets the floor", async () => {
69
- globalThis.fetch = vi.fn().mockResolvedValue({
70
- ok: true,
71
- json: () => Promise.resolve({ minSupported: "0.1.0" }),
72
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
- });
74
- const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
75
- const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => { }));
76
- const { enforceMinVersion } = await import("./version-check.js");
77
- await enforceMinVersion("0.1.0", "https://api.example.com");
78
- expect(exitSpy).not.toHaveBeenCalled();
79
- expect(errSpy).not.toHaveBeenCalled();
80
- });
81
- it("falls back to stale cache on network error", async () => {
82
- // Seed an older cached floor for THIS server URL that already proved
83
- // this version stale.
84
- const url = "https://api.example.com";
85
- mkdirSync(join(tmpHome, ".kubeagent"), { recursive: true });
86
- writeFileSync(join(tmpHome, ".kubeagent", "version-check.json"), JSON.stringify({
87
- [url]: {
88
- // checkedAt > 24h ago so we attempt a refetch
89
- checkedAt: Date.now() - 48 * 60 * 60 * 1000,
90
- minSupported: "0.5.0",
91
- },
92
- }), "utf-8");
93
- globalThis.fetch = vi.fn().mockRejectedValue(new Error("offline"));
94
- const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
95
- const exitSpy = vi.spyOn(process, "exit").mockImplementation(((_code) => {
96
- throw new Error("__exit__");
97
- }));
98
- const { enforceMinVersion } = await import("./version-check.js");
99
- await expect(enforceMinVersion("0.1.0", url)).rejects.toThrow("__exit__");
100
- expect(exitSpy).toHaveBeenCalledWith(1);
101
- expect(errSpy).toHaveBeenCalled();
102
- });
103
- it("returns silently with no cache and a network error", async () => {
104
- globalThis.fetch = vi.fn().mockRejectedValue(new Error("offline"));
105
- const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
106
- const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => { }));
107
- const { enforceMinVersion } = await import("./version-check.js");
108
- await enforceMinVersion("0.1.0", "https://api.example.com");
109
- expect(exitSpy).not.toHaveBeenCalled();
110
- expect(errSpy).not.toHaveBeenCalled();
111
- });
112
- it("uses cached value within 24h without refetching", async () => {
113
- const url = "https://api.example.com";
114
- mkdirSync(join(tmpHome, ".kubeagent"), { recursive: true });
115
- writeFileSync(join(tmpHome, ".kubeagent", "version-check.json"), JSON.stringify({ [url]: { checkedAt: Date.now(), minSupported: "0.2.0" } }), "utf-8");
116
- const fetchSpy = vi.fn();
117
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
118
- globalThis.fetch = fetchSpy;
119
- const exitSpy = vi.spyOn(process, "exit").mockImplementation(((_code) => {
120
- throw new Error("__exit__");
121
- }));
122
- vi.spyOn(console, "error").mockImplementation(() => { });
123
- const { enforceMinVersion } = await import("./version-check.js");
124
- await expect(enforceMinVersion("0.1.0", url)).rejects.toThrow("__exit__");
125
- expect(fetchSpy).not.toHaveBeenCalled();
126
- expect(exitSpy).toHaveBeenCalledWith(1);
127
- });
128
- it("writes cache after a successful fetch", async () => {
129
- globalThis.fetch = vi.fn().mockResolvedValue({
130
- ok: true,
131
- json: () => Promise.resolve({ minSupported: "0.1.0" }),
132
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
- });
134
- vi.spyOn(process, "exit").mockImplementation((() => { }));
135
- vi.spyOn(console, "error").mockImplementation(() => { });
136
- const { enforceMinVersion } = await import("./version-check.js");
137
- await enforceMinVersion("0.1.5", "https://api.example.com");
138
- const cachePath = join(tmpHome, ".kubeagent", "version-check.json");
139
- expect(existsSync(cachePath)).toBe(true);
140
- const cache = JSON.parse(readFileSync(cachePath, "utf-8"));
141
- expect(cache["https://api.example.com"].minSupported).toBe("0.1.0");
142
- });
143
- it("does not apply a floor cached for a different server URL", async () => {
144
- const otherUrl = "https://other-server.example.com";
145
- mkdirSync(join(tmpHome, ".kubeagent"), { recursive: true });
146
- writeFileSync(join(tmpHome, ".kubeagent", "version-check.json"), JSON.stringify({
147
- [otherUrl]: { checkedAt: Date.now(), minSupported: "99.0.0" },
148
- }), "utf-8");
149
- // Target server returns a permissive floor; other-server's strict floor
150
- // must NOT be applied here.
151
- globalThis.fetch = vi.fn().mockResolvedValue({
152
- ok: true,
153
- json: () => Promise.resolve({ minSupported: "0.1.0" }),
154
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
- });
156
- const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => { }));
157
- vi.spyOn(console, "error").mockImplementation(() => { });
158
- const { enforceMinVersion } = await import("./version-check.js");
159
- await enforceMinVersion("0.1.0", "https://api.example.com");
160
- expect(exitSpy).not.toHaveBeenCalled();
161
- });
162
- it("ignores the legacy single-entry cache shape", async () => {
163
- // Old format: {checkedAt, minSupported} at the root. Should be discarded
164
- // since we don't know which server it came from.
165
- mkdirSync(join(tmpHome, ".kubeagent"), { recursive: true });
166
- writeFileSync(join(tmpHome, ".kubeagent", "version-check.json"), JSON.stringify({ checkedAt: Date.now(), minSupported: "99.0.0" }), "utf-8");
167
- globalThis.fetch = vi.fn().mockResolvedValue({
168
- ok: true,
169
- json: () => Promise.resolve({ minSupported: "0.1.0" }),
170
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
- });
172
- const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => { }));
173
- vi.spyOn(console, "error").mockImplementation(() => { });
174
- const { enforceMinVersion } = await import("./version-check.js");
175
- await enforceMinVersion("0.1.0", "https://api.example.com");
176
- // Legacy floor (99.0.0) ignored, fresh fetch (0.1.0) applied — no exit.
177
- expect(exitSpy).not.toHaveBeenCalled();
178
- });
179
- });