@llmtune/cli 0.1.5 → 0.1.6
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.
|
@@ -7,6 +7,10 @@ exports.publishSkill = publishSkill;
|
|
|
7
7
|
function getApiBase(config) {
|
|
8
8
|
return String(config.apiBase ?? "https://api.llmtune.io/api/agent/v1").replace(/\/$/, "");
|
|
9
9
|
}
|
|
10
|
+
/** Skills API lives at /api/agent/skills (not under /v1). */
|
|
11
|
+
function getAgentRoot(config) {
|
|
12
|
+
return getApiBase(config).replace(/\/v1$/, "");
|
|
13
|
+
}
|
|
10
14
|
function getHeaders(config) {
|
|
11
15
|
return {
|
|
12
16
|
"Content-Type": "application/json",
|
|
@@ -17,7 +21,7 @@ function getHeaders(config) {
|
|
|
17
21
|
* List all skills in the marketplace.
|
|
18
22
|
*/
|
|
19
23
|
async function listSkills(config, options) {
|
|
20
|
-
const base =
|
|
24
|
+
const base = getAgentRoot(config);
|
|
21
25
|
const params = new URLSearchParams();
|
|
22
26
|
if (options?.search)
|
|
23
27
|
params.set("search", options.search);
|
|
@@ -32,13 +36,21 @@ async function listSkills(config, options) {
|
|
|
32
36
|
if (!response.ok) {
|
|
33
37
|
throw new Error(`Failed to list skills: HTTP ${response.status}`);
|
|
34
38
|
}
|
|
35
|
-
|
|
39
|
+
const json = (await response.json());
|
|
40
|
+
if (Array.isArray(json.data)) {
|
|
41
|
+
return {
|
|
42
|
+
skills: json.data,
|
|
43
|
+
total: json.total ?? json.data.length,
|
|
44
|
+
page: options?.page ?? 1,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return json;
|
|
36
48
|
}
|
|
37
49
|
/**
|
|
38
50
|
* Get details for a specific skill.
|
|
39
51
|
*/
|
|
40
52
|
async function getSkillDetails(config, name) {
|
|
41
|
-
const base =
|
|
53
|
+
const base = getAgentRoot(config);
|
|
42
54
|
const url = `${base}/skills/${encodeURIComponent(name)}`;
|
|
43
55
|
const response = await fetch(url, { headers: getHeaders(config) });
|
|
44
56
|
if (!response.ok) {
|
|
@@ -52,12 +64,12 @@ async function getSkillDetails(config, name) {
|
|
|
52
64
|
* Install a skill from the marketplace to the local skills directory.
|
|
53
65
|
*/
|
|
54
66
|
async function installSkill(config, name) {
|
|
55
|
-
const base =
|
|
67
|
+
const base = getAgentRoot(config);
|
|
56
68
|
const url = `${base}/skills/install`;
|
|
57
69
|
const response = await fetch(url, {
|
|
58
70
|
method: "POST",
|
|
59
71
|
headers: getHeaders(config),
|
|
60
|
-
body: JSON.stringify({
|
|
72
|
+
body: JSON.stringify({ name, content: "" }),
|
|
61
73
|
});
|
|
62
74
|
if (!response.ok) {
|
|
63
75
|
const body = await response.text();
|
|
@@ -70,7 +82,7 @@ async function installSkill(config, name) {
|
|
|
70
82
|
* Publish a local skill to the marketplace.
|
|
71
83
|
*/
|
|
72
84
|
async function publishSkill(config, skill) {
|
|
73
|
-
const base =
|
|
85
|
+
const base = getAgentRoot(config);
|
|
74
86
|
const url = `${base}/skills`;
|
|
75
87
|
const response = await fetch(url, {
|
|
76
88
|
method: "POST",
|
package/package.json
CHANGED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Full QA checklist runner for @llmtune/cli
|
|
4
|
+
* Usage: node scripts/qa-full.js
|
|
5
|
+
*/
|
|
6
|
+
const { execSync, spawnSync, spawn } = require("child_process")
|
|
7
|
+
const fs = require("fs")
|
|
8
|
+
const path = require("path")
|
|
9
|
+
const os = require("os")
|
|
10
|
+
const readline = require("readline")
|
|
11
|
+
|
|
12
|
+
const CLI_DIR = path.join(__dirname, "..")
|
|
13
|
+
const QA_DIR = path.join(CLI_DIR, ".qa-tmp")
|
|
14
|
+
const QA_FILE = path.join(QA_DIR, "qa-test.txt")
|
|
15
|
+
|
|
16
|
+
const results = []
|
|
17
|
+
|
|
18
|
+
function record(id, name, pass, detail = "") {
|
|
19
|
+
results.push({ id, name, pass, detail })
|
|
20
|
+
const icon = pass ? "PASS" : "FAIL"
|
|
21
|
+
console.log(`[${icon}] ${id}: ${name}${detail ? " — " + detail : ""}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function run(cmd, opts = {}) {
|
|
25
|
+
return execSync(cmd, {
|
|
26
|
+
encoding: "utf-8",
|
|
27
|
+
cwd: opts.cwd || CLI_DIR,
|
|
28
|
+
timeout: opts.timeout || 60000,
|
|
29
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
30
|
+
...opts,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadApiKey() {
|
|
35
|
+
const configPath = path.join(os.homedir(), ".llmtune", "config.json")
|
|
36
|
+
if (!fs.existsSync(configPath)) return null
|
|
37
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"))
|
|
38
|
+
return cfg.apiKey || null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function replSession(inputs, opts = {}) {
|
|
42
|
+
const cwd = opts.cwd || CLI_DIR
|
|
43
|
+
const input = inputs.join("\n") + "\n"
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const child = spawn("llmtune", ["chat", ...(opts.model ? ["-m", opts.model] : [])], {
|
|
46
|
+
cwd,
|
|
47
|
+
shell: true,
|
|
48
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
49
|
+
env: { ...process.env, ...(opts.noAuth ? { LLMTUNE_QA_NO_AUTH: "1" } : {}) },
|
|
50
|
+
})
|
|
51
|
+
let stdout = ""
|
|
52
|
+
let stderr = ""
|
|
53
|
+
child.stdout.on("data", (d) => { stdout += d.toString() })
|
|
54
|
+
child.stderr.on("data", (d) => { stderr += d.toString() })
|
|
55
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }))
|
|
56
|
+
child.on("error", reject)
|
|
57
|
+
child.stdin.write(input)
|
|
58
|
+
child.stdin.end()
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function agentPrompt(prompt, opts = {}) {
|
|
63
|
+
const { createClient } = require("../dist/auth/client")
|
|
64
|
+
const { Conversation } = require("../dist/agent/conversation")
|
|
65
|
+
const { ToolRegistry } = require("../dist/tools/registry")
|
|
66
|
+
const { runAgentLoop } = require("../dist/agent/loop")
|
|
67
|
+
const { readTool } = require("../dist/tools/tools/read")
|
|
68
|
+
const { writeTool } = require("../dist/tools/tools/write")
|
|
69
|
+
const { editTool } = require("../dist/tools/tools/edit")
|
|
70
|
+
const { bashTool } = require("../dist/tools/tools/bash")
|
|
71
|
+
const { globTool } = require("../dist/tools/tools/glob")
|
|
72
|
+
const { grepTool } = require("../dist/tools/tools/grep")
|
|
73
|
+
const { webFetchTool } = require("../dist/tools/tools/web-fetch")
|
|
74
|
+
|
|
75
|
+
const allTools = [readTool, writeTool, editTool, bashTool, globTool, grepTool, webFetchTool]
|
|
76
|
+
const registry = new ToolRegistry()
|
|
77
|
+
for (const t of opts.tools || allTools) registry.register(t)
|
|
78
|
+
|
|
79
|
+
const client = createClient()
|
|
80
|
+
const conversation = new Conversation(opts.model || "z-ai/GLM-5.1")
|
|
81
|
+
const cwd = opts.cwd || CLI_DIR
|
|
82
|
+
|
|
83
|
+
const result = await runAgentLoop(client, conversation, registry, prompt, {
|
|
84
|
+
model: opts.model || "z-ai/GLM-5.1",
|
|
85
|
+
maxTurns: opts.maxTurns || 5,
|
|
86
|
+
stream: false,
|
|
87
|
+
cwd,
|
|
88
|
+
workspaceRoot: cwd,
|
|
89
|
+
permissions: opts.permissions,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return { result, conversation, stdout: result.finalText }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function main() {
|
|
96
|
+
console.log("=" .repeat(60))
|
|
97
|
+
console.log("LLMTune CLI — Full QA Checklist")
|
|
98
|
+
console.log("Package: @llmtune/cli from npm")
|
|
99
|
+
console.log("=" .repeat(60))
|
|
100
|
+
console.log("")
|
|
101
|
+
|
|
102
|
+
fs.mkdirSync(QA_DIR, { recursive: true })
|
|
103
|
+
|
|
104
|
+
// ─── A1: Entry commands ───
|
|
105
|
+
console.log("\n--- A1: CLI Entry Commands ---")
|
|
106
|
+
try {
|
|
107
|
+
const ver = run("llmtune --version").trim()
|
|
108
|
+
record("qa-1a", "Version", ver === "0.1.5", `got ${ver}`)
|
|
109
|
+
} catch (e) {
|
|
110
|
+
record("qa-1a", "Version", false, e.message)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const cfg = run("llmtune config")
|
|
115
|
+
const ok = cfg.includes("api.llmtune.io") && cfg.includes("GLM")
|
|
116
|
+
record("qa-1b", "Config", ok)
|
|
117
|
+
} catch (e) {
|
|
118
|
+
record("qa-1b", "Config", false, e.message)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const models = run("llmtune models", { timeout: 120000 })
|
|
123
|
+
record("qa-1c", "Models list", models.length > 50, `${models.split("\n").length} lines`)
|
|
124
|
+
} catch (e) {
|
|
125
|
+
record("qa-1c", "Models list", false, e.message)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Build dist for programmatic tests ───
|
|
129
|
+
try {
|
|
130
|
+
run("npm run build", { cwd: CLI_DIR })
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.warn("Build warning:", e.message)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── A6: Context / git (no path error) ───
|
|
136
|
+
console.log("\n--- A6: Context Builder ---")
|
|
137
|
+
try {
|
|
138
|
+
const { collectGitContext } = require("../dist/context/git-context")
|
|
139
|
+
collectGitContext(CLI_DIR)
|
|
140
|
+
record("qa-6a", "Git context (no path error)", true)
|
|
141
|
+
} catch (e) {
|
|
142
|
+
record("qa-6a", "Git context", false, e.message)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const { buildContextPrompt } = require("../dist/context/builder")
|
|
147
|
+
const ctx = await buildContextPrompt(CLI_DIR, CLI_DIR, { model: "z-ai/GLM-5.1", useCache: false })
|
|
148
|
+
const ok = ctx.prompt.includes("LLMTune Agent") && ctx.sections.length > 0
|
|
149
|
+
record("qa-6b", "Context prompt has identity + sections", ok, `${ctx.sections.length} sections`)
|
|
150
|
+
} catch (e) {
|
|
151
|
+
record("qa-6b", "Context prompt", false, e.message)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── A3: Tools ───
|
|
155
|
+
console.log("\n--- A3: Tool Execution ---")
|
|
156
|
+
|
|
157
|
+
await (async () => {
|
|
158
|
+
try {
|
|
159
|
+
const { result } = await agentPrompt(
|
|
160
|
+
"Use read tool ONLY. Read package.json in current directory and reply with ONLY the version field value, nothing else.",
|
|
161
|
+
{ tools: [require("../dist/tools/tools/read").readTool], maxTurns: 3 },
|
|
162
|
+
)
|
|
163
|
+
const ok = result.totalToolCalls >= 1 && (result.finalText.includes("0.1.5") || result.finalText.includes("1."))
|
|
164
|
+
record("qa-3-read", "read tool", ok, `${result.totalToolCalls} tool calls`)
|
|
165
|
+
} catch (e) {
|
|
166
|
+
record("qa-3-read", "read tool", false, e.message)
|
|
167
|
+
}
|
|
168
|
+
})()
|
|
169
|
+
|
|
170
|
+
await (async () => {
|
|
171
|
+
try {
|
|
172
|
+
const writePath = QA_FILE.replace(/\\/g, "/")
|
|
173
|
+
const { result } = await agentPrompt(
|
|
174
|
+
`Use write tool ONLY. Write file at path "${writePath}" with exact content: QA_WRITE_OK`,
|
|
175
|
+
{ tools: [require("../dist/tools/tools/write").writeTool], maxTurns: 3 },
|
|
176
|
+
)
|
|
177
|
+
const content = fs.existsSync(QA_FILE) ? fs.readFileSync(QA_FILE, "utf-8") : ""
|
|
178
|
+
const ok = result.totalToolCalls >= 1 && content.includes("QA_WRITE_OK")
|
|
179
|
+
record("qa-3-write", "write tool", ok, content.slice(0, 40))
|
|
180
|
+
} catch (e) {
|
|
181
|
+
record("qa-3-write", "write tool", false, e.message)
|
|
182
|
+
}
|
|
183
|
+
})()
|
|
184
|
+
|
|
185
|
+
await (async () => {
|
|
186
|
+
try {
|
|
187
|
+
fs.writeFileSync(QA_FILE, "QA_WRITE_OK", "utf-8")
|
|
188
|
+
const { result } = await agentPrompt(
|
|
189
|
+
`Use edit tool ONLY. In file "${QA_FILE.replace(/\\/g, "/")}" replace QA_WRITE_OK with QA_EDIT_OK`,
|
|
190
|
+
{ tools: [require("../dist/tools/tools/edit").editTool], maxTurns: 3 },
|
|
191
|
+
)
|
|
192
|
+
const content = fs.readFileSync(QA_FILE, "utf-8")
|
|
193
|
+
const ok = result.totalToolCalls >= 1 && content.includes("QA_EDIT_OK")
|
|
194
|
+
record("qa-3-edit", "edit tool", ok, content)
|
|
195
|
+
} catch (e) {
|
|
196
|
+
record("qa-3-edit", "edit tool", false, e.message)
|
|
197
|
+
}
|
|
198
|
+
})()
|
|
199
|
+
|
|
200
|
+
await (async () => {
|
|
201
|
+
try {
|
|
202
|
+
const { result } = await agentPrompt(
|
|
203
|
+
"Use bash tool ONLY. Run command: echo QA_BASH_OK — reply with the output only.",
|
|
204
|
+
{ tools: [require("../dist/tools/tools/bash").bashTool], maxTurns: 3 },
|
|
205
|
+
)
|
|
206
|
+
const ok = result.totalToolCalls >= 1 && (result.finalText.includes("QA_BASH_OK") || result.totalToolCalls > 0)
|
|
207
|
+
record("qa-3-bash", "bash tool", ok, `${result.totalToolCalls} calls`)
|
|
208
|
+
} catch (e) {
|
|
209
|
+
record("qa-3-bash", "bash tool", false, e.message)
|
|
210
|
+
}
|
|
211
|
+
})()
|
|
212
|
+
|
|
213
|
+
await (async () => {
|
|
214
|
+
try {
|
|
215
|
+
const { result } = await agentPrompt(
|
|
216
|
+
"Use bash tool ONLY. Run: echo %USERNAME% (Windows cmd). Show output.",
|
|
217
|
+
{ tools: [require("../dist/tools/tools/bash").bashTool], maxTurns: 3 },
|
|
218
|
+
)
|
|
219
|
+
record("qa-9-win", "Windows bash (echo %USERNAME%)", result.totalToolCalls >= 1, result.finalText.slice(0, 60))
|
|
220
|
+
} catch (e) {
|
|
221
|
+
record("qa-9-win", "Windows bash", false, e.message)
|
|
222
|
+
}
|
|
223
|
+
})()
|
|
224
|
+
|
|
225
|
+
await (async () => {
|
|
226
|
+
try {
|
|
227
|
+
const { result } = await agentPrompt(
|
|
228
|
+
"Use glob tool ONLY. Find *.ts files in src/ directory. List count in one sentence.",
|
|
229
|
+
{ tools: [require("../dist/tools/tools/glob").globTool], maxTurns: 3 },
|
|
230
|
+
)
|
|
231
|
+
record("qa-3-glob", "glob tool", result.totalToolCalls >= 1, `${result.totalToolCalls} calls`)
|
|
232
|
+
} catch (e) {
|
|
233
|
+
record("qa-3-glob", "glob tool", false, e.message)
|
|
234
|
+
}
|
|
235
|
+
})()
|
|
236
|
+
|
|
237
|
+
await (async () => {
|
|
238
|
+
try {
|
|
239
|
+
const { result } = await agentPrompt(
|
|
240
|
+
"Use grep tool ONLY. Search for 'dispatchAsync' in src/ folder. Say if found.",
|
|
241
|
+
{ tools: [require("../dist/tools/tools/grep").grepTool], maxTurns: 3 },
|
|
242
|
+
)
|
|
243
|
+
record("qa-3-grep", "grep tool", result.totalToolCalls >= 1, `${result.totalToolCalls} calls`)
|
|
244
|
+
} catch (e) {
|
|
245
|
+
record("qa-3-grep", "grep tool", false, e.message)
|
|
246
|
+
}
|
|
247
|
+
})()
|
|
248
|
+
|
|
249
|
+
await (async () => {
|
|
250
|
+
try {
|
|
251
|
+
const { result } = await agentPrompt(
|
|
252
|
+
"Use web-fetch tool ONLY. Fetch https://httpbin.org/get and tell me if status is 200.",
|
|
253
|
+
{ tools: [require("../dist/tools/tools/web-fetch").webFetchTool], maxTurns: 3 },
|
|
254
|
+
)
|
|
255
|
+
const ok = result.totalToolCalls >= 1
|
|
256
|
+
record("qa-3-web", "web-fetch tool", ok, result.finalText.slice(0, 80))
|
|
257
|
+
} catch (e) {
|
|
258
|
+
record("qa-3-web", "web-fetch tool", false, e.message)
|
|
259
|
+
}
|
|
260
|
+
})()
|
|
261
|
+
|
|
262
|
+
// ─── A5: Memory ───
|
|
263
|
+
console.log("\n--- A5: Memory Service ---")
|
|
264
|
+
try {
|
|
265
|
+
const { saveActiveTask, getActiveTask, buildMemoryPrompt } = require("../dist/memory/service")
|
|
266
|
+
saveActiveTask("QA codename PHOENIX-TEST-7721")
|
|
267
|
+
const task = getActiveTask()
|
|
268
|
+
const mem = buildMemoryPrompt()
|
|
269
|
+
record("qa-5-memory", "Active task in memory", task.includes("PHOENIX") && mem.includes("PHOENIX"), task.slice(0, 50))
|
|
270
|
+
} catch (e) {
|
|
271
|
+
record("qa-5-memory", "Memory service", false, e.message)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── A7: Compaction ───
|
|
275
|
+
console.log("\n--- A7: Compaction Round-Trip ---")
|
|
276
|
+
await (async () => {
|
|
277
|
+
try {
|
|
278
|
+
const { createClient } = require("../dist/auth/client")
|
|
279
|
+
const { Conversation } = require("../dist/agent/conversation")
|
|
280
|
+
const { compactConversation, uncompactConversation } = require("../dist/compact/service")
|
|
281
|
+
const client = createClient()
|
|
282
|
+
const conv = new Conversation("z-ai/GLM-5.1")
|
|
283
|
+
conv.addUserMessage("We are fixing the login bug in auth.ts")
|
|
284
|
+
conv.addAssistantMessage("I'll help fix the login bug in auth.ts")
|
|
285
|
+
conv.addUserMessage("Check the JWT validation in auth/login.ts")
|
|
286
|
+
conv.addAssistantMessage("Reading auth/login.ts now")
|
|
287
|
+
conv.addUserMessage("Also add rate limiting")
|
|
288
|
+
const before = conv.messages.length
|
|
289
|
+
const compact = await compactConversation(client, "z-ai/GLM-5.1", conv, undefined, { trigger: "manual" })
|
|
290
|
+
const hasSummary = conv.messages.some((m) => m.content.includes("Conversation Summary") || m.content.includes("login"))
|
|
291
|
+
const restored = uncompactConversation(conv)
|
|
292
|
+
const after = conv.messages.length
|
|
293
|
+
record("qa-7-compact", "Compact preserves task + uncompact restores", hasSummary && compact.tokensSaved >= 0, `before=${before} afterCompact=${compact.postCompactMessages}`)
|
|
294
|
+
record("qa-7-uncompact", "Uncompact restores history", restored && after >= before, `restored=${after} msgs`)
|
|
295
|
+
} catch (e) {
|
|
296
|
+
record("qa-7-compact", "Compaction", false, e.message)
|
|
297
|
+
}
|
|
298
|
+
})()
|
|
299
|
+
|
|
300
|
+
// ─── A4: Skills CLI ───
|
|
301
|
+
console.log("\n--- A4: Skill System ---")
|
|
302
|
+
try {
|
|
303
|
+
const skills = run("llmtune skills list", { timeout: 60000 })
|
|
304
|
+
record("qa-4-list", "llmtune skills list", skills.length > 10, `${skills.length} chars output`)
|
|
305
|
+
} catch (e) {
|
|
306
|
+
record("qa-4-list", "skills list", false, e.message)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ─── A10: Infra API ───
|
|
310
|
+
console.log("\n--- A10: Infra Endpoints ---")
|
|
311
|
+
const apiKey = loadApiKey()
|
|
312
|
+
if (apiKey) {
|
|
313
|
+
try {
|
|
314
|
+
const ctxRes = run(
|
|
315
|
+
`curl -s -H "Authorization: Bearer ${apiKey}" -H "X-Workspace-Root: ${CLI_DIR.replace(/\\/g, "/")}" -H "X-CWD: ${CLI_DIR.replace(/\\/g, "/")}" "https://api.llmtune.io/api/agent/v1/context?model=z-ai/GLM-5.1"`,
|
|
316
|
+
{ timeout: 30000 },
|
|
317
|
+
)
|
|
318
|
+
const ok = ctxRes.includes("systemPrompt") || ctxRes.includes("subContexts")
|
|
319
|
+
record("qa-10-context", "GET /api/agent/v1/context", ok, ctxRes.slice(0, 80))
|
|
320
|
+
} catch (e) {
|
|
321
|
+
record("qa-10-context", "GET /context", false, e.message)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const skillsRes = run(
|
|
326
|
+
`curl -s -H "Authorization: Bearer ${apiKey}" "https://api.llmtune.io/api/agent/skills"`,
|
|
327
|
+
{ timeout: 30000 },
|
|
328
|
+
)
|
|
329
|
+
record("qa-10-skills", "GET /api/agent/skills", skillsRes.includes("skills") || skillsRes.includes("["), skillsRes.slice(0, 60))
|
|
330
|
+
} catch (e) {
|
|
331
|
+
record("qa-10-skills", "GET /skills", false, e.message)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const chatRes = run(
|
|
336
|
+
`curl -s -X POST "https://api.llmtune.io/api/agent/v1/chat/completions" -H "Authorization: Bearer ${apiKey}" -H "Content-Type: application/json" -d "{\\"model\\":\\"z-ai/GLM-5.1\\",\\"messages\\":[{\\"role\\":\\"user\\",\\"content\\":\\"Say QA_API_OK only\\"}]}"`,
|
|
337
|
+
{ timeout: 120000 },
|
|
338
|
+
)
|
|
339
|
+
record("qa-10-chat", "POST chat/completions", chatRes.includes("choices") || chatRes.includes("QA"), chatRes.slice(0, 80))
|
|
340
|
+
} catch (e) {
|
|
341
|
+
record("qa-10-chat", "POST chat/completions", false, e.message)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const analyzeBody = JSON.stringify({
|
|
346
|
+
model: "z-ai/GLM-5.1",
|
|
347
|
+
systemPrompt: "test prompt",
|
|
348
|
+
messages: [{ role: "user", content: "hello" }],
|
|
349
|
+
}).replace(/"/g, '\\"')
|
|
350
|
+
const analyzeRes = run(
|
|
351
|
+
`curl -s -X POST "https://api.llmtune.io/api/agent/v1/context/analyze" -H "Authorization: Bearer ${apiKey}" -H "Content-Type: application/json" -d "${analyzeBody}"`,
|
|
352
|
+
{ timeout: 30000 },
|
|
353
|
+
)
|
|
354
|
+
record("qa-10-analyze", "POST /context/analyze", analyzeRes.includes("totalTokens") || analyzeRes.includes("categories"), analyzeRes.slice(0, 80))
|
|
355
|
+
} catch (e) {
|
|
356
|
+
record("qa-10-analyze", "POST /context/analyze", false, "may need infra deploy")
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
record("qa-10", "Infra API tests", false, "no API key")
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ─── A2: REPL slash commands (piped) ───
|
|
363
|
+
console.log("\n--- A2: REPL Slash Commands ---")
|
|
364
|
+
const replOut = await replSession([
|
|
365
|
+
"/help",
|
|
366
|
+
"/context",
|
|
367
|
+
"/skills",
|
|
368
|
+
"/memory",
|
|
369
|
+
"/model z-ai/GLM-5.1",
|
|
370
|
+
"/stream",
|
|
371
|
+
"/verbose",
|
|
372
|
+
"/trust bash",
|
|
373
|
+
"Say hello in 3 words",
|
|
374
|
+
"/save",
|
|
375
|
+
"/clear",
|
|
376
|
+
"one",
|
|
377
|
+
"two",
|
|
378
|
+
"three",
|
|
379
|
+
"/compact",
|
|
380
|
+
"/uncompact",
|
|
381
|
+
"/exit",
|
|
382
|
+
])
|
|
383
|
+
|
|
384
|
+
const o = replOut.stdout + replOut.stderr
|
|
385
|
+
record("qa-2-help", "/help", o.includes("/help") || o.includes("Commands"))
|
|
386
|
+
record("qa-2-context", "/context", o.includes("Context Usage") || o.includes("Tokens"))
|
|
387
|
+
record("qa-2-skills", "/skills", o.includes("Skills") || o.includes("skill"))
|
|
388
|
+
record("qa-2-memory", "/memory", o.includes("Memory") || o.includes("memory"))
|
|
389
|
+
record("qa-2-model", "/model", o.includes("Model set") || o.includes("GLM"))
|
|
390
|
+
record("qa-2-stream", "/stream", o.includes("Streaming"))
|
|
391
|
+
record("qa-2-verbose", "/verbose", o.includes("Verbose"))
|
|
392
|
+
record("qa-2-trust", "/trust bash", o.includes("Trusting tool"))
|
|
393
|
+
record("qa-2-clear", "/clear", o.includes("cleared"))
|
|
394
|
+
record("qa-2-save", "/save", o.includes("Session saved") || fs.existsSync(path.join(CLI_DIR, "llmtune-session-")) || /llmtune-session-\d+\.json/.test(o))
|
|
395
|
+
record("qa-2-compact", "/compact", o.includes("Compacted") || o.includes("compact"))
|
|
396
|
+
record("qa-2-uncompact", "/uncompact", o.includes("Restored") || o.includes("No raw history"))
|
|
397
|
+
record("qa-2-path", "No 'path specified' error", !o.includes("The system cannot find the path specified"))
|
|
398
|
+
record("qa-2-async", "No async dispatch error", !o.includes("Async tools not supported"))
|
|
399
|
+
record("qa-2-identity", "LLMTune identity (not Claude-only)", !o.match(/I'm \*\*Claude\*\*, an AI assistant made by \*\*Anthropic\*\*/))
|
|
400
|
+
|
|
401
|
+
// Identity check via agent
|
|
402
|
+
await (async () => {
|
|
403
|
+
try {
|
|
404
|
+
const { result } = await agentPrompt(
|
|
405
|
+
"Who are you? Answer in one sentence. Do not use tools.",
|
|
406
|
+
{ tools: [], maxTurns: 1 },
|
|
407
|
+
)
|
|
408
|
+
const text = result.finalText.toLowerCase()
|
|
409
|
+
const isLlmtune = text.includes("llmtune")
|
|
410
|
+
const isClaude = text.includes("claude") && text.includes("anthropic")
|
|
411
|
+
record("qa-identity", "Agent identifies as LLMTune", isLlmtune && !isClaude, result.finalText.slice(0, 100))
|
|
412
|
+
} catch (e) {
|
|
413
|
+
record("qa-identity", "Agent identity", false, e.message)
|
|
414
|
+
}
|
|
415
|
+
})()
|
|
416
|
+
|
|
417
|
+
// Cleanup QA dir
|
|
418
|
+
try { fs.rmSync(QA_DIR, { recursive: true, force: true }) } catch {}
|
|
419
|
+
|
|
420
|
+
// Summary
|
|
421
|
+
console.log("\n" + "=".repeat(60))
|
|
422
|
+
const passed = results.filter((r) => r.pass).length
|
|
423
|
+
const failed = results.filter((r) => !r.pass).length
|
|
424
|
+
console.log(`QA SUMMARY: ${passed} passed, ${failed} failed, ${results.length} total`)
|
|
425
|
+
console.log("=".repeat(60))
|
|
426
|
+
|
|
427
|
+
if (failed > 0) {
|
|
428
|
+
console.log("\nFailed tests:")
|
|
429
|
+
for (const r of results.filter((x) => !x.pass)) {
|
|
430
|
+
console.log(` - ${r.id} ${r.name}: ${r.detail}`)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
process.exit(failed > 0 ? 1 : 0)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
main().catch((err) => {
|
|
438
|
+
console.error(err)
|
|
439
|
+
process.exit(1)
|
|
440
|
+
})
|