@krishivpb60/aether-ai-cli 1.0.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.
@@ -0,0 +1,182 @@
1
+ import os from "node:os";
2
+ import { join } from "node:path";
3
+ import { mkdir, rm } from "node:fs/promises";
4
+ import { test, before, after, beforeEach, afterEach } from "node:test";
5
+ import assert from "node:assert";
6
+
7
+ // Redirect homedir before importing config.js to point to a temporary test folder
8
+ const tempHome = join(process.cwd(), "temp-test-home");
9
+ process.env.USERPROFILE = tempHome;
10
+ process.env.HOME = tempHome;
11
+
12
+ // Dynamically import config.js so it gets the redirected homedir
13
+ const {
14
+ getConfigPath,
15
+ loadConfig,
16
+ saveConfig,
17
+ getConfigValue,
18
+ setConfigValue,
19
+ deleteConfigValue,
20
+ resetConfig,
21
+ listConfig,
22
+ getAIConfig,
23
+ configExists,
24
+ isValidConfigKey,
25
+ loadHistory,
26
+ saveHistory,
27
+ clearHistory,
28
+ } = await import("../src/config.js");
29
+
30
+ const { getAllConfigKeys } = await import("../src/ai/providers.js");
31
+
32
+ test("Configuration Loading Suite", async (t) => {
33
+ let originalEnv = {};
34
+ const configKeys = getAllConfigKeys();
35
+
36
+ before(async () => {
37
+ await mkdir(tempHome, { recursive: true });
38
+ // Backup relevant env vars
39
+ for (const key of configKeys) {
40
+ if (key in process.env) {
41
+ originalEnv[key] = process.env[key];
42
+ }
43
+ }
44
+ });
45
+
46
+ after(async () => {
47
+ await rm(tempHome, { recursive: true, force: true });
48
+ });
49
+
50
+ beforeEach(async () => {
51
+ await resetConfig();
52
+ // Clear config env vars
53
+ for (const key of configKeys) {
54
+ delete process.env[key];
55
+ }
56
+ delete process.env.CUSTOM_TEST_API_KEY;
57
+ });
58
+
59
+ afterEach(async () => {
60
+ await resetConfig();
61
+ // Restore original env vars
62
+ for (const key of configKeys) {
63
+ if (key in originalEnv) {
64
+ process.env[key] = originalEnv[key];
65
+ } else {
66
+ delete process.env[key];
67
+ }
68
+ }
69
+ });
70
+
71
+ await t.test("getConfigPath should return path inside temporary home", () => {
72
+ const path = getConfigPath();
73
+ assert.ok(path.startsWith(tempHome));
74
+ assert.ok(path.endsWith(join(".aether", "config.json")));
75
+ });
76
+
77
+ await t.test("loadConfig should return empty object if file does not exist", async () => {
78
+ const config = await loadConfig();
79
+ assert.deepStrictEqual(config, {});
80
+ });
81
+
82
+ await t.test("saveConfig and loadConfig should save and load config file", async () => {
83
+ const testConfig = { GROQ_API_KEY: "test-groq-key", OPENAI_MODEL: "gpt-4" };
84
+ await saveConfig(testConfig);
85
+ assert.strictEqual(await configExists(), true);
86
+
87
+ const loaded = await loadConfig();
88
+ assert.deepStrictEqual(loaded, testConfig);
89
+ });
90
+
91
+ await t.test("getConfigValue and setConfigValue read and write specific keys", async () => {
92
+ await setConfigValue("TOGETHER_API_KEY", "together-val");
93
+ const val = await getConfigValue("TOGETHER_API_KEY");
94
+ assert.strictEqual(val, "together-val");
95
+
96
+ await deleteConfigValue("TOGETHER_API_KEY");
97
+ const deletedVal = await getConfigValue("TOGETHER_API_KEY");
98
+ assert.strictEqual(deletedVal, undefined);
99
+ });
100
+
101
+ await t.test("listConfig should mask sensitive keys", async () => {
102
+ await saveConfig({
103
+ GROQ_API_KEY: "supersecretgroqkey",
104
+ OPENAI_MODEL: "gpt-4o",
105
+ OTHER_PROP: "not-sensitive-but-short",
106
+ SHORT_SECRET: "123", // too short to mask (<= 8 chars)
107
+ });
108
+
109
+ const masked = await listConfig();
110
+ assert.strictEqual(masked.OPENAI_MODEL, "gpt-4o");
111
+ assert.strictEqual(masked.OTHER_PROP, "not-sensitive-but-short");
112
+ assert.strictEqual(masked.SHORT_SECRET, "123");
113
+ // GROQ_API_KEY contains "KEY", so it is sensitive. It has length 18 (> 8).
114
+ // It should be masked: first 6 chars + "•••" + last 3 chars
115
+ // "supers" + "•••" + "key" = "supers•••key"
116
+ assert.strictEqual(masked.GROQ_API_KEY, "supers•••key");
117
+ });
118
+
119
+ await t.test("getAIConfig Priority: config file overrides process.env", async () => {
120
+ // 1. Set environment variables
121
+ process.env.GROQ_API_KEY = "env-groq-key";
122
+ process.env.OPENAI_API_KEY = "env-openai-key";
123
+
124
+ // 2. Set config file value for GROQ_API_KEY (should override env)
125
+ // but leave OPENAI_API_KEY empty in config (should fall back to env)
126
+ await saveConfig({
127
+ GROQ_API_KEY: "file-groq-key",
128
+ });
129
+
130
+ const aiConfig = await getAIConfig();
131
+ assert.strictEqual(aiConfig.GROQ_API_KEY, "file-groq-key"); // overrides
132
+ assert.strictEqual(aiConfig.OPENAI_API_KEY, "env-openai-key"); // falls back
133
+ });
134
+
135
+ await t.test("getAIConfig supports fallback for any custom key ending with _API_KEY from process.env", async () => {
136
+ process.env.CUSTOM_TEST_API_KEY = "custom-env-key";
137
+ const aiConfig = await getAIConfig();
138
+ assert.strictEqual(aiConfig.CUSTOM_TEST_API_KEY, "custom-env-key");
139
+ });
140
+
141
+ await t.test("isValidConfigKey checks key formats and known keys", () => {
142
+ assert.strictEqual(isValidConfigKey("GROQ_API_KEY"), true);
143
+ assert.strictEqual(isValidConfigKey("CUSTOM_API_KEY"), true);
144
+ assert.strictEqual(isValidConfigKey("OPENAI_MODEL"), true);
145
+ assert.strictEqual(isValidConfigKey("GOOGLE_API_KEYS"), true);
146
+ assert.strictEqual(isValidConfigKey("THEME"), true);
147
+ assert.strictEqual(isValidConfigKey("CUSTOM_COMMANDS"), true);
148
+ assert.strictEqual(isValidConfigKey("INVALID_KEY_NAME"), false);
149
+ });
150
+
151
+ await t.test("loadHistory, saveHistory, and clearHistory write and delete log files", async () => {
152
+ // 1. Initial state: history should be empty
153
+ const initial = await loadHistory();
154
+ assert.deepStrictEqual(initial, []);
155
+
156
+ // 2. Save history
157
+ const testHistory = [
158
+ { role: "user", content: "ping" },
159
+ { role: "assistant", content: "pong" }
160
+ ];
161
+ await saveHistory(testHistory);
162
+
163
+ // 3. Load back
164
+ const loaded = await loadHistory();
165
+ assert.deepStrictEqual(loaded, testHistory);
166
+
167
+ // 4. Clear history
168
+ await clearHistory();
169
+ const cleared = await loadHistory();
170
+ assert.deepStrictEqual(cleared, []);
171
+ });
172
+
173
+ await t.test("getAIConfig supports and loads CUSTOM_COMMANDS correctly", async () => {
174
+ const testCommands = { "/explain": "Explain this:", "/refactor": "Refactor this:" };
175
+ await saveConfig({
176
+ CUSTOM_COMMANDS: testCommands
177
+ });
178
+
179
+ const config = await getAIConfig();
180
+ assert.deepStrictEqual(config.CUSTOM_COMMANDS, testCommands);
181
+ });
182
+ });
@@ -0,0 +1,105 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert";
3
+ import {
4
+ detectMathExpression,
5
+ solveMath,
6
+ generateKryloReply,
7
+ runMainframeHack,
8
+ } from "../src/ai/fallback.js";
9
+
10
+ test("Offline Math Fallback & Krylo Suite", async (t) => {
11
+ await t.test("detectMathExpression identifies valid mathematical expressions", () => {
12
+ assert.strictEqual(detectMathExpression("2 + 2"), "2+2");
13
+ assert.strictEqual(detectMathExpression("10 * (5 - 3)"), "10*(5-3)");
14
+ assert.strictEqual(detectMathExpression("2 ^ 3"), "2^3");
15
+ assert.strictEqual(detectMathExpression("5 % 2"), "5%2");
16
+ });
17
+
18
+ await t.test("detectMathExpression rejects non-math and invalid expressions", () => {
19
+ assert.strictEqual(detectMathExpression("hello world"), null);
20
+ assert.strictEqual(detectMathExpression("2 + 2 = 4"), null);
21
+ assert.strictEqual(detectMathExpression("x + y"), null);
22
+ assert.strictEqual(detectMathExpression("123"), null); // No operators
23
+ assert.strictEqual(detectMathExpression("2 + a"), null);
24
+ });
25
+
26
+ await t.test("solveMath evaluates simple math expressions correctly", () => {
27
+ const result = solveMath("2+2");
28
+ assert.ok(result);
29
+ assert.strictEqual(result.type, "local-math");
30
+ assert.ok(result.text.includes("Expression: 2+2"));
31
+ assert.ok(result.text.includes("Result: 4"));
32
+ });
33
+
34
+ await t.test("solveMath converts ^ to ** correctly for exponentiation", () => {
35
+ const result = solveMath("2^3");
36
+ assert.ok(result);
37
+ assert.ok(result.text.includes("Result: 8"));
38
+ });
39
+
40
+ await t.test("solveMath handles floats and division", () => {
41
+ const result = solveMath("5/2");
42
+ assert.ok(result);
43
+ assert.ok(result.text.includes("Result: 2.5"));
44
+ });
45
+
46
+ await t.test("solveMath returns null on syntax error or unsafe code", () => {
47
+ assert.strictEqual(solveMath("2++2"), null);
48
+ assert.strictEqual(solveMath("2/0"), null); // Division by zero -> Infinity (isFinite checks false)
49
+ assert.strictEqual(solveMath(""), null);
50
+ assert.strictEqual(solveMath("console.log(1)"), null);
51
+ });
52
+
53
+ await t.test("generateKryloReply responds to help and shortcut keywords", () => {
54
+ const reply = generateKryloReply("I need help with commands");
55
+ assert.strictEqual(reply.type, "krylo-local");
56
+ assert.ok(reply.text.includes("[SYSTEM DECK CHEAT SHEET]"));
57
+ assert.ok(reply.text.includes("Ctrl + K"));
58
+ });
59
+
60
+ await t.test("generateKryloReply responds to status and diagnostic keywords", () => {
61
+ const reply = generateKryloReply("What is the CPU status?");
62
+ assert.strictEqual(reply.type, "krylo-local");
63
+ assert.ok(reply.text.includes("[LIVE DIAGNOSTIC READOUT]"));
64
+ assert.ok(reply.text.includes("Failover Mesh"));
65
+ });
66
+
67
+ await t.test("generateKryloReply responds to matrix/rain/color keywords", () => {
68
+ const reply = generateKryloReply("change matrix color");
69
+ assert.strictEqual(reply.type, "krylo-local");
70
+ assert.ok(reply.text.includes("[NEURAL GRIDS MODULATION]"));
71
+ assert.ok(reply.text.includes("Classic Green"));
72
+ });
73
+
74
+ await t.test("generateKryloReply responds to who/name/creator keywords", () => {
75
+ const reply = generateKryloReply("who is your creator?");
76
+ assert.strictEqual(reply.type, "krylo-local");
77
+ assert.ok(reply.text.includes("[HOLOGRAPHIC COMPANION PROTOCOL]"));
78
+ assert.ok(reply.text.includes("Krishiv PB"));
79
+ });
80
+
81
+ await t.test("generateKryloReply falls back to random terminal responses", () => {
82
+ const reply = generateKryloReply("Unrelated query");
83
+ assert.strictEqual(reply.type, "krylo-local");
84
+ assert.ok(reply.text.includes("[KRYLO TERMINAL RESPONSE]"));
85
+ });
86
+
87
+ await t.test("detectMathExpression and solveMath support trig, logs, square root and constants", () => {
88
+ assert.strictEqual(detectMathExpression("sin(pi / 2)"), "sin(pi/2)");
89
+ assert.strictEqual(detectMathExpression("sqrt(9) + abs(-5)"), "sqrt(9)+abs(-5)");
90
+
91
+ const resTrig = solveMath("sin(pi/2)");
92
+ assert.ok(resTrig);
93
+ assert.ok(resTrig.text.includes("Result: 1"));
94
+
95
+ const resComplex = solveMath("sqrt(9) + abs(-5)");
96
+ assert.ok(resComplex);
97
+ assert.ok(resComplex.text.includes("Result: 8"));
98
+ });
99
+
100
+ await t.test("runMainframeHack returns the game intro template", () => {
101
+ const gameIntro = runMainframeHack();
102
+ assert.strictEqual(gameIntro.type, "mainframe-game");
103
+ assert.ok(gameIntro.text.includes("Objective: Bypass security"));
104
+ });
105
+ });
@@ -0,0 +1,136 @@
1
+ import { test, before, after } from "node:test";
2
+ import assert from "node:assert";
3
+ import { join } from "node:path";
4
+ import { writeFile, mkdir, rm } from "node:fs/promises";
5
+ import { parseFile, formatContext } from "../src/file-parser.js";
6
+
7
+ const tempDir = join(process.cwd(), "temp-test-files");
8
+
9
+ test("File Parser & Context Suite", async (t) => {
10
+ before(async () => {
11
+ await mkdir(tempDir, { recursive: true });
12
+ });
13
+
14
+ after(async () => {
15
+ await rm(tempDir, { recursive: true, force: true });
16
+ });
17
+
18
+ await t.test("parseFile parses a standard .txt file successfully", async () => {
19
+ const filePath = join(tempDir, "test.txt");
20
+ const content = "Hello World Aether AI";
21
+ await writeFile(filePath, content, "utf-8");
22
+
23
+ const parsed = await parseFile(filePath);
24
+ assert.strictEqual(parsed.name, "test.txt");
25
+ assert.strictEqual(parsed.content, content);
26
+ assert.strictEqual(parsed.extension, ".txt");
27
+ assert.strictEqual(parsed.size, Buffer.byteLength(content));
28
+ });
29
+
30
+ await t.test("parseFile parses a code .js file successfully", async () => {
31
+ const filePath = join(tempDir, "test.js");
32
+ const content = "const x = 42;\nconsole.log(x);";
33
+ await writeFile(filePath, content, "utf-8");
34
+
35
+ const parsed = await parseFile(filePath);
36
+ assert.strictEqual(parsed.name, "test.js");
37
+ assert.strictEqual(parsed.content, content);
38
+ assert.strictEqual(parsed.extension, ".js");
39
+ });
40
+
41
+ await t.test("parseFile parses a .json file successfully", async () => {
42
+ const filePath = join(tempDir, "test.json");
43
+ const content = '{\n "cyberpunk": "active"\n}';
44
+ await writeFile(filePath, content, "utf-8");
45
+
46
+ const parsed = await parseFile(filePath);
47
+ assert.strictEqual(parsed.name, "test.json");
48
+ assert.strictEqual(parsed.content, content.trim());
49
+ assert.strictEqual(parsed.extension, ".json");
50
+ });
51
+
52
+ await t.test("parseFile parses a .csv file successfully", async () => {
53
+ const filePath = join(tempDir, "test.csv");
54
+ const content = "id,name\n1,aether\n2,krylo";
55
+ await writeFile(filePath, content, "utf-8");
56
+
57
+ const parsed = await parseFile(filePath);
58
+ assert.strictEqual(parsed.name, "test.csv");
59
+ assert.strictEqual(parsed.content, content);
60
+ assert.strictEqual(parsed.extension, ".csv");
61
+ });
62
+
63
+ await t.test("parseFile truncates content exceeding 30,000 characters", async () => {
64
+ const filePath = join(tempDir, "large.txt");
65
+ const longContent = "A".repeat(35000);
66
+ await writeFile(filePath, longContent, "utf-8");
67
+
68
+ const parsed = await parseFile(filePath);
69
+ assert.strictEqual(parsed.name, "large.txt");
70
+
71
+ const expectedPrefix = "A".repeat(30000);
72
+ const expectedTruncationSuffix = "\n\n[... truncated at 30,000 characters]";
73
+ assert.strictEqual(parsed.content, expectedPrefix + expectedTruncationSuffix);
74
+ });
75
+
76
+ await t.test("parseFile throws error on unsupported extension", async () => {
77
+ const filePath = join(tempDir, "test.xyz");
78
+ await writeFile(filePath, "some content", "utf-8");
79
+
80
+ await assert.rejects(
81
+ parseFile(filePath),
82
+ /Unsupported file type: "\.xyz"/
83
+ );
84
+ });
85
+
86
+ await t.test("parseFile throws error when file does not exist", async () => {
87
+ const nonExistentPath = join(tempDir, "does-not-exist.txt");
88
+ await assert.rejects(
89
+ parseFile(nonExistentPath),
90
+ /File not found:/
91
+ );
92
+ });
93
+
94
+ await t.test("parseFile throws error on directory path without supported extension", async () => {
95
+ await assert.rejects(
96
+ parseFile(tempDir),
97
+ /Unsupported file type:/
98
+ );
99
+ });
100
+
101
+ await t.test("parseFile throws error on directory path with supported extension", async () => {
102
+ const dirWithExt = join(tempDir, "test-dir.js");
103
+ await mkdir(dirWithExt, { recursive: true });
104
+ await assert.rejects(
105
+ parseFile(dirWithExt),
106
+ /Not a file:/
107
+ );
108
+ });
109
+
110
+ await t.test("formatContext returns formatted template string", () => {
111
+ const fileData = {
112
+ name: "test.txt",
113
+ content: "Hello from inside the file.",
114
+ size: 27,
115
+ extension: ".txt",
116
+ };
117
+
118
+ const formatted = formatContext(fileData);
119
+ const expectedLines = [
120
+ "[Context File: test.txt (27B, .txt)]",
121
+ "---",
122
+ "Hello from inside the file.",
123
+ "---",
124
+ "[End of test.txt]",
125
+ ];
126
+ assert.strictEqual(formatted, expectedLines.join("\n"));
127
+ });
128
+
129
+ await t.test("formatContext formats KB/MB file sizes correctly", () => {
130
+ const dataKb = { name: "kb.txt", content: "", size: 1536, extension: ".txt" };
131
+ assert.ok(formatContext(dataKb).includes("(1.5KB, .txt)"));
132
+
133
+ const dataMb = { name: "mb.txt", content: "", size: 1048576 * 2.5, extension: ".txt" };
134
+ assert.ok(formatContext(dataMb).includes("(2.5MB, .txt)"));
135
+ });
136
+ });
@@ -0,0 +1,174 @@
1
+ import { test, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { routePrompt } from "../src/ai/router.js";
4
+
5
+ const originalFetch = globalThis.fetch;
6
+
7
+ test("Universal AI Router Suite", async (t) => {
8
+ let fetchCalls = [];
9
+
10
+ beforeEach(() => {
11
+ fetchCalls = [];
12
+ });
13
+
14
+ afterEach(() => {
15
+ globalThis.fetch = originalFetch;
16
+ });
17
+
18
+ await t.test("routePrompt routes to local math solver when pure math expression", async () => {
19
+ globalThis.fetch = async () => {
20
+ throw new Error("Fetch should not be called for local math solver");
21
+ };
22
+
23
+ const result = await routePrompt("5 * 5", "You are a helpful assistant", {});
24
+ assert.strictEqual(result.provider, "local");
25
+ assert.strictEqual(result.node, 0);
26
+ assert.strictEqual(result.type, "local-math");
27
+ assert.ok(result.text.includes("Result: 25"));
28
+ });
29
+
30
+ await t.test("routePrompt routes to active providers in order of priority", async () => {
31
+ globalThis.fetch = async (url, options) => {
32
+ fetchCalls.push({ url, options });
33
+ if (url.includes("api.groq.com")) {
34
+ return {
35
+ ok: true,
36
+ json: async () => ({
37
+ choices: [{ message: { content: "Groq success response" } }],
38
+ }),
39
+ };
40
+ }
41
+ return { ok: false, status: 400 };
42
+ };
43
+
44
+ const config = {
45
+ GROQ_API_KEY: "groq-ok-key",
46
+ OPENAI_API_KEY: "openai-ok-key",
47
+ };
48
+
49
+ const result = await routePrompt("What is the capital of France?", "Sys prompt", config);
50
+
51
+ assert.strictEqual(result.provider, "groq");
52
+ assert.strictEqual(result.text, "Groq success response");
53
+ assert.strictEqual(result.node, 1);
54
+
55
+ // Verify only Groq was called, OpenAI was skipped since Groq succeeded
56
+ assert.strictEqual(fetchCalls.length, 1);
57
+ assert.ok(fetchCalls[0].url.includes("api.groq.com"));
58
+ });
59
+
60
+ await t.test("routePrompt falls back to next provider if priority provider fails", async () => {
61
+ globalThis.fetch = async (url, options) => {
62
+ fetchCalls.push({ url, options });
63
+ if (url.includes("api.groq.com")) {
64
+ return {
65
+ ok: false,
66
+ status: 500,
67
+ statusText: "Internal Server Error",
68
+ text: async () => "Groq overloaded",
69
+ };
70
+ }
71
+ if (url.includes("api.openai.com")) {
72
+ return {
73
+ ok: true,
74
+ json: async () => ({
75
+ choices: [{ message: { content: "OpenAI success response" } }],
76
+ }),
77
+ };
78
+ }
79
+ return { ok: false, status: 400 };
80
+ };
81
+
82
+ const config = {
83
+ GROQ_API_KEY: "groq-fail-key",
84
+ OPENAI_API_KEY: "openai-ok-key",
85
+ };
86
+
87
+ const result = await routePrompt("Explain quantum computing", "Sys prompt", config);
88
+
89
+ assert.strictEqual(result.provider, "openai");
90
+ assert.strictEqual(result.text, "OpenAI success response");
91
+ // Node index increments for failed providers, so OpenAI should be node 2
92
+ assert.strictEqual(result.node, 2);
93
+
94
+ // Verify both were called in order
95
+ assert.strictEqual(fetchCalls.length, 2);
96
+ assert.ok(fetchCalls[0].url.includes("api.groq.com"));
97
+ assert.ok(fetchCalls[1].url.includes("api.openai.com"));
98
+ });
99
+
100
+ await t.test("routePrompt handles Google extra key rotation and failover", async () => {
101
+ globalThis.fetch = async (url, options) => {
102
+ fetchCalls.push({ url, options });
103
+ if (url.includes("key=key-fail")) {
104
+ return {
105
+ ok: false,
106
+ status: 403,
107
+ statusText: "Forbidden",
108
+ text: async () => "Invalid Key",
109
+ };
110
+ }
111
+ if (url.includes("key=key-success")) {
112
+ return {
113
+ ok: true,
114
+ json: async () => ({
115
+ candidates: [{ content: { parts: [{ text: "Gemini success response" }] } }],
116
+ }),
117
+ };
118
+ }
119
+ return { ok: false, status: 400 };
120
+ };
121
+
122
+ const config = {
123
+ GOOGLE_API_KEYS: "key-fail, key-success",
124
+ };
125
+
126
+ const result = await routePrompt("Tell me a joke", "Sys prompt", config);
127
+
128
+ assert.strictEqual(result.provider, "google");
129
+ assert.strictEqual(result.text, "Gemini success response");
130
+ assert.strictEqual(result.node, 2);
131
+
132
+ assert.strictEqual(fetchCalls.length, 2);
133
+ assert.ok(fetchCalls[0].url.includes("key=key-fail"));
134
+ assert.ok(fetchCalls[1].url.includes("key=key-success"));
135
+ });
136
+
137
+ await t.test("routePrompt falls back to Krylo companion when no providers are configured", async () => {
138
+ globalThis.fetch = async () => {
139
+ throw new Error("Fetch should not be called");
140
+ };
141
+
142
+ const result = await routePrompt("status", "Sys prompt", {});
143
+ assert.strictEqual(result.provider, "krylo-fallback");
144
+ assert.strictEqual(result.node, 0);
145
+ assert.strictEqual(result.type, "krylo-local");
146
+ assert.ok(result.text.includes("[LIVE DIAGNOSTIC READOUT]"));
147
+ });
148
+
149
+ await t.test("routePrompt falls back to Krylo companion when all providers fail", async () => {
150
+ globalThis.fetch = async (url, options) => {
151
+ fetchCalls.push({ url, options });
152
+ return {
153
+ ok: false,
154
+ status: 500,
155
+ statusText: "Service Unavailable",
156
+ text: async () => "Server Down",
157
+ };
158
+ };
159
+
160
+ const config = {
161
+ GROQ_API_KEY: "groq-bad",
162
+ OPENAI_API_KEY: "openai-bad",
163
+ };
164
+
165
+ const result = await routePrompt("Hello", "Sys prompt", config);
166
+
167
+ assert.strictEqual(result.provider, "krylo-fallback");
168
+ assert.strictEqual(result.node, 0);
169
+ assert.ok(result.errors);
170
+ assert.strictEqual(result.errors.length, 2);
171
+ assert.ok(result.errors[0].includes("Node 1 Groq"));
172
+ assert.ok(result.errors[1].includes("Node 2 OpenAI"));
173
+ });
174
+ });