Skip to content

Commit 5c2ddeb

Browse files
committed
feat: add testing framework and enhance auth config management
- Introduced Vitest as a testing framework and added a test suite for the `buildAuthConfig` function to ensure proper handling of authentication configurations. - Updated `package.json` to include Vitest and a test script. - Modified `tsconfig.json` to exclude test files from compilation. - Refactored the `buildAuthConfig` function in `auth.ts` for improved clarity and maintainability, ensuring it correctly manages voice tools in the configuration.
1 parent fb94401 commit 5c2ddeb

4 files changed

Lines changed: 183 additions & 58 deletions

File tree

‎package.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
],
2626
"scripts": {
2727
"build": "tsc",
28+
"test": "vitest run",
2829
"prepublishOnly": "npm run build"
2930
},
3031
"keywords": [
@@ -52,6 +53,7 @@
5253
"pine-voice": "^0.1.3"
5354
},
5455
"devDependencies": {
55-
"typescript": "^5.0.0"
56+
"typescript": "^5.0.0",
57+
"vitest": "^4.0.18"
5658
}
5759
}

‎src/auth.test.ts‎

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, it, expect } from "vitest";
2+
import { buildAuthConfig } from "./auth.js";
3+
4+
const ALL_VOICE_TOOLS = [
5+
"pine_voice_call_and_wait",
6+
"pine_voice_call",
7+
"pine_voice_call_status",
8+
];
9+
10+
describe("buildAuthConfig", () => {
11+
const token = "tok_abc";
12+
const userId = "user_123";
13+
14+
it("adds credentials to an empty config", () => {
15+
const { updatedConfig, addedTools } = buildAuthConfig({}, token, userId);
16+
17+
expect(updatedConfig.plugins.entries["openclaw-pine-voice"].config).toEqual({
18+
access_token: token,
19+
user_id: userId,
20+
});
21+
expect(addedTools).toEqual(ALL_VOICE_TOOLS);
22+
expect(updatedConfig.tools.allow).toEqual(ALL_VOICE_TOOLS);
23+
});
24+
25+
it("preserves existing config keys", () => {
26+
const cfg = {
27+
someOtherKey: "keep me",
28+
plugins: { entries: { "other-plugin": { enabled: true } } },
29+
tools: { timeout: 30 },
30+
};
31+
const { updatedConfig } = buildAuthConfig(cfg, token, userId);
32+
33+
expect(updatedConfig.someOtherKey).toBe("keep me");
34+
expect(updatedConfig.plugins.entries["other-plugin"]).toEqual({ enabled: true });
35+
expect(updatedConfig.tools.timeout).toBe(30);
36+
});
37+
38+
it("merges with existing tools.allow without duplicates", () => {
39+
const cfg = {
40+
tools: { allow: ["some_other_tool", "pine_voice_call"] },
41+
};
42+
const { updatedConfig, addedTools } = buildAuthConfig(cfg, token, userId);
43+
44+
expect(addedTools).toEqual(["pine_voice_call_and_wait", "pine_voice_call_status"]);
45+
expect(updatedConfig.tools.allow).toEqual([
46+
"some_other_tool",
47+
"pine_voice_call",
48+
"pine_voice_call_and_wait",
49+
"pine_voice_call_status",
50+
]);
51+
});
52+
53+
it("returns empty addedTools when all voice tools already present", () => {
54+
const cfg = {
55+
tools: { allow: [...ALL_VOICE_TOOLS, "extra_tool"] },
56+
};
57+
const { updatedConfig, addedTools } = buildAuthConfig(cfg, token, userId);
58+
59+
expect(addedTools).toEqual([]);
60+
expect(updatedConfig.tools.allow).toEqual([...ALL_VOICE_TOOLS, "extra_tool"]);
61+
});
62+
63+
it("filters out non-string values from existing allow list", () => {
64+
const cfg = {
65+
tools: { allow: ["valid_tool", 42, null, undefined, { bad: true }] },
66+
};
67+
const { updatedConfig } = buildAuthConfig(cfg, token, userId);
68+
69+
expect(updatedConfig.tools.allow).toEqual([
70+
"valid_tool",
71+
...ALL_VOICE_TOOLS,
72+
]);
73+
});
74+
75+
it("handles non-array tools.allow gracefully", () => {
76+
const cfg = { tools: { allow: "not-an-array" } };
77+
const { updatedConfig, addedTools } = buildAuthConfig(cfg, token, userId);
78+
79+
expect(addedTools).toEqual(ALL_VOICE_TOOLS);
80+
expect(updatedConfig.tools.allow).toEqual(ALL_VOICE_TOOLS);
81+
});
82+
83+
it("overwrites previous credentials in existing plugin entry", () => {
84+
const cfg = {
85+
plugins: {
86+
entries: {
87+
"openclaw-pine-voice": {
88+
config: { access_token: "old_token", user_id: "old_user", extra: true },
89+
},
90+
},
91+
},
92+
};
93+
const { updatedConfig } = buildAuthConfig(cfg, token, userId);
94+
const pluginConfig = updatedConfig.plugins.entries["openclaw-pine-voice"].config;
95+
96+
expect(pluginConfig.access_token).toBe(token);
97+
expect(pluginConfig.user_id).toBe(userId);
98+
expect(pluginConfig.extra).toBe(true);
99+
});
100+
101+
it("handles missing tools key entirely", () => {
102+
const cfg = { plugins: {} };
103+
const { updatedConfig, addedTools } = buildAuthConfig(cfg, token, userId);
104+
105+
expect(addedTools).toEqual(ALL_VOICE_TOOLS);
106+
expect(updatedConfig.tools.allow).toEqual(ALL_VOICE_TOOLS);
107+
});
108+
});

‎src/auth.ts‎

Lines changed: 70 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,62 @@ import { PineVoice, AuthError } from "pine-voice";
1818
// ---------------------------------------------------------------------------
1919
const pendingAuth = new Map<string, string>(); // email → requestToken
2020

21+
// ---------------------------------------------------------------------------
22+
// Shared helpers
23+
// ---------------------------------------------------------------------------
24+
25+
const VOICE_TOOLS = [
26+
"pine_voice_call_and_wait",
27+
"pine_voice_call",
28+
"pine_voice_call_status",
29+
] as const;
30+
31+
/**
32+
* Build an updated config that stores credentials and ensures voice tools
33+
* are present in `tools.allow`. Pure function — no side effects.
34+
*/
35+
export function buildAuthConfig(
36+
cfg: Record<string, any>,
37+
accessToken: string,
38+
userId: string,
39+
): { updatedConfig: Record<string, any>; addedTools: string[] } {
40+
const plugins = (cfg.plugins ?? {}) as Record<string, any>;
41+
const entries = (plugins.entries ?? {}) as Record<string, any>;
42+
const pluginEntry = (entries["openclaw-pine-voice"] ?? {}) as Record<string, any>;
43+
const tools = (cfg.tools ?? {}) as Record<string, any>;
44+
const existingAllow = Array.isArray(tools.allow)
45+
? tools.allow.filter((t: unknown): t is string => typeof t === "string")
46+
: [];
47+
48+
const addedTools = VOICE_TOOLS.filter(t => !existingAllow.includes(t));
49+
const mergedAllow = [...existingAllow, ...addedTools];
50+
51+
return {
52+
updatedConfig: {
53+
...cfg,
54+
plugins: {
55+
...plugins,
56+
entries: {
57+
...entries,
58+
"openclaw-pine-voice": {
59+
...pluginEntry,
60+
config: {
61+
...(pluginEntry.config ?? {}),
62+
access_token: accessToken,
63+
user_id: userId,
64+
},
65+
},
66+
},
67+
},
68+
tools: {
69+
...tools,
70+
allow: mergedAllow,
71+
},
72+
},
73+
addedTools: [...addedTools],
74+
};
75+
}
76+
2177
// ---------------------------------------------------------------------------
2278
// Tool registration (primary path)
2379
// ---------------------------------------------------------------------------
@@ -107,54 +163,18 @@ export function registerAuthTools(api: any) {
107163
params.code,
108164
);
109165

110-
// Write credentials + ensure voice tools are in tools.allow
111166
const cfg = api.runtime.config.loadConfig();
112-
const plugins = (cfg.plugins ?? {}) as Record<string, any>;
113-
const entries = (plugins.entries ?? {}) as Record<string, any>;
114-
const pluginEntry = (entries["openclaw-pine-voice"] ?? {}) as Record<string, any>;
115-
const tools = (cfg.tools ?? {}) as Record<string, any>;
116-
const existingAllow = Array.isArray(tools.allow) ? tools.allow as string[] : [];
117-
118-
const requiredTools = [
119-
"pine_voice_call_and_wait",
120-
"pine_voice_call",
121-
"pine_voice_call_status",
122-
];
123-
const missingTools = requiredTools.filter(t => !existingAllow.includes(t));
124-
const mergedAllow = [...existingAllow, ...missingTools];
125-
126-
const updatedConfig = {
127-
...cfg,
128-
plugins: {
129-
...plugins,
130-
entries: {
131-
...entries,
132-
"openclaw-pine-voice": {
133-
...pluginEntry,
134-
config: {
135-
...(pluginEntry.config ?? {}),
136-
access_token: accessToken,
137-
user_id: userId,
138-
},
139-
},
140-
},
141-
},
142-
tools: {
143-
...tools,
144-
allow: mergedAllow,
145-
},
146-
};
147-
167+
const { updatedConfig, addedTools } = buildAuthConfig(cfg, accessToken, userId);
148168
await api.runtime.config.writeConfigFile(updatedConfig);
149169
pendingAuth.delete(params.email);
150170

151-
const toolsNote = missingTools.length > 0
152-
? ` Voice tools (${missingTools.join(", ")}) have been added to tools.allow.`
171+
const toolsNote = addedTools.length > 0
172+
? ` Voice tools (${addedTools.join(", ")}) have been added to tools.allow.`
153173
: "";
154174

155175
api.log?.info?.(`pine-voice: auth successful for ${params.email}, credentials saved`);
156-
if (missingTools.length > 0) {
157-
api.log?.info?.(`pine-voice: added ${missingTools.join(", ")} to tools.allow`);
176+
if (addedTools.length > 0) {
177+
api.log?.info?.(`pine-voice: added ${addedTools.join(", ")} to tools.allow`);
158178
}
159179

160180
return {
@@ -239,21 +259,15 @@ export function registerAuthCommands(api: any) {
239259
opts.code,
240260
);
241261

242-
console.log("Authentication successful!");
243-
console.log(`Add this to your plugin config in ~/.openclaw/openclaw.json:`);
244-
console.log("");
245-
console.log(` "plugins": {`);
246-
console.log(` "entries": {`);
247-
console.log(` "openclaw-pine-voice": {`);
248-
console.log(` "config": {`);
249-
console.log(` "access_token": "${accessToken}",`);
250-
console.log(` "user_id": "${userId}"`);
251-
console.log(` }`);
252-
console.log(` }`);
253-
console.log(` }`);
254-
console.log(` }`);
255-
console.log("");
256-
console.log("Then restart the gateway:");
262+
const cfg = api.runtime.config.loadConfig();
263+
const { updatedConfig, addedTools } = buildAuthConfig(cfg, accessToken, userId);
264+
await api.runtime.config.writeConfigFile(updatedConfig);
265+
266+
console.log("Authentication successful! Credentials saved to openclaw.json.");
267+
if (addedTools.length > 0) {
268+
console.log(`Voice tools (${addedTools.join(", ")}) added to tools.allow.`);
269+
}
270+
console.log("\nRestart the gateway for changes to take effect:");
257271
console.log(" openclaw gateway restart");
258272
} catch (err: unknown) {
259273
const message = err instanceof Error ? err.message : String(err);

‎tsconfig.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
"outDir": "dist",
1010
"declaration": true
1111
},
12-
"include": ["src/**/*"]
12+
"include": ["src/**/*"],
13+
"exclude": ["src/**/*.test.ts"]
1314
}

0 commit comments

Comments
 (0)