cue-ai 0.5.0 → 0.7.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/README.md +757 -110
- package/package.json +5 -5
- package/profiles/README.md +12 -12
- package/profiles/SCHEMA.md +31 -3
- package/profiles/_cache/README.md +1 -1
- package/profiles/_types.ts +26 -1
- package/profiles/backend/profile.yaml +1 -0
- package/profiles/career/profile.yaml +13 -0
- package/profiles/core/profile.yaml +76 -9
- package/profiles/creative-media/README.md +1 -1
- package/profiles/cybersecurity/profile.yaml +779 -756
- package/profiles/ecc/profile.yaml +39 -0
- package/profiles/event-design/profile.yaml +10 -0
- package/profiles/fleet-control/README.md +1 -1
- package/profiles/frontend/profile.yaml +14 -0
- package/profiles/full/README.md +1 -1
- package/profiles/go-api/profile.yaml +1 -0
- package/profiles/marketing/profile.yaml +12 -1
- package/profiles/predict-everything/profile.yaml +9 -0
- package/profiles/rust/profile.yaml +22 -3
- package/profiles/rust-cli/profile.yaml +14 -0
- package/profiles/rust-core/profile.yaml +35 -0
- package/profiles/rust-embedded/profile.yaml +11 -0
- package/profiles/rust-ffi/profile.yaml +13 -0
- package/profiles/rust-game/profile.yaml +11 -0
- package/profiles/rust-wasm/profile.yaml +11 -0
- package/profiles/rust-web/profile.yaml +17 -0
- package/profiles/schema.json +44 -4
- package/profiles/trendradar/profile.yaml +11 -0
- package/resources/mcps/README.md +39 -164
- package/resources/mcps/configs/claude.sanitized.json +55 -0
- package/resources/mcps/configs/claude_runtime.sanitized.json +47 -0
- package/resources/skills/README.md +70 -113
- package/resources/skills/skills/event-design/wedding-invitations/SKILL.md +43 -0
- package/resources/skills/skills/meta/acpx/SKILL.md +78 -0
- package/resources/skills/skills/meta/cue-usage/SKILL.md +24 -0
- package/resources/skills/skills/meta/profile-fit-monitor/SKILL.md +24 -0
- package/resources/skills/skills/predict-everything/mirofish/SKILL.md +75 -0
- package/resources/skills/skills/research/trendradar/SKILL.md +88 -0
- package/resources/skills/skills/rust/async-tokio/SKILL.md +27 -0
- package/resources/skills/skills/rust/axum-api/SKILL.md +38 -0
- package/resources/skills/skills/rust/bacon-watch/SKILL.md +24 -0
- package/resources/skills/skills/rust/bevy/SKILL.md +43 -0
- package/resources/skills/skills/rust/bindgen/SKILL.md +39 -0
- package/resources/skills/skills/rust/cargo-audit/SKILL.md +26 -0
- package/resources/skills/skills/rust/cargo-basics/SKILL.md +28 -0
- package/resources/skills/skills/rust/cargo-chef/SKILL.md +43 -0
- package/resources/skills/skills/rust/cargo-edit/SKILL.md +26 -0
- package/resources/skills/skills/rust/cargo-expand/SKILL.md +24 -0
- package/resources/skills/skills/rust/cargo-flamegraph/SKILL.md +26 -0
- package/resources/skills/skills/rust/cargo-fuzz/SKILL.md +34 -0
- package/resources/skills/skills/rust/cargo-hack/SKILL.md +26 -0
- package/resources/skills/skills/rust/cargo-msrv/SKILL.md +30 -0
- package/resources/skills/skills/rust/cargo-mutants/SKILL.md +26 -0
- package/resources/skills/skills/rust/cargo-nextest/SKILL.md +24 -0
- package/resources/skills/skills/rust/cargo-readme/SKILL.md +36 -0
- package/resources/skills/skills/rust/cbindgen/SKILL.md +41 -0
- package/resources/skills/skills/rust/chisel-tool/SKILL.md +32 -0
- package/resources/skills/skills/rust/clap-cli/SKILL.md +44 -0
- package/resources/skills/skills/rust/clippy-and-fmt/SKILL.md +25 -0
- package/resources/skills/skills/rust/cross-compile/SKILL.md +26 -0
- package/resources/skills/skills/rust/embedded/SKILL.md +33 -0
- package/resources/skills/skills/rust/error-handling/SKILL.md +32 -0
- package/resources/skills/skills/rust/just-runner/SKILL.md +26 -0
- package/resources/skills/skills/rust/mdbook/SKILL.md +25 -0
- package/resources/skills/skills/rust/napi-rs/SKILL.md +32 -0
- package/resources/skills/skills/rust/no-std/SKILL.md +42 -0
- package/resources/skills/skills/rust/property-testing/SKILL.md +35 -0
- package/resources/skills/skills/rust/pyo3/SKILL.md +40 -0
- package/resources/skills/skills/rust/ratatui-tui/SKILL.md +36 -0
- package/resources/skills/skills/rust/release-plz/SKILL.md +27 -0
- package/resources/skills/skills/rust/reqwest/SKILL.md +37 -0
- package/resources/skills/skills/rust/sccache/SKILL.md +28 -0
- package/resources/skills/skills/rust/serde/SKILL.md +30 -0
- package/resources/skills/skills/rust/snapshot-testing/SKILL.md +30 -0
- package/resources/skills/skills/rust/sqlx-cli/SKILL.md +33 -0
- package/resources/skills/skills/rust/tracing/SKILL.md +36 -0
- package/resources/skills/skills/rust/typos-spellcheck/SKILL.md +31 -0
- package/resources/skills/skills/rust/uniffi/SKILL.md +38 -0
- package/resources/skills/skills/rust/wasm-rust/SKILL.md +27 -0
- package/resources/skills/skills/security/agentshield/SKILL.md +119 -0
- package/src/commands/_index.ts +47 -3
- package/src/commands/cli.test.ts +192 -0
- package/src/commands/cli.ts +303 -0
- package/src/commands/current.ts +1 -1
- package/src/commands/debug.test.ts +62 -0
- package/src/commands/debug.ts +212 -0
- package/src/commands/discover.scoring.test.ts +216 -0
- package/src/commands/discover.test.ts +145 -0
- package/src/commands/discover.ts +2618 -0
- package/src/commands/eval-behavior.test.ts +56 -0
- package/src/commands/eval-behavior.ts +189 -0
- package/src/commands/eval.test.ts +102 -0
- package/src/commands/eval.ts +348 -0
- package/src/commands/evolve.ts +291 -0
- package/src/commands/failures.test.ts +78 -0
- package/src/commands/failures.ts +393 -0
- package/src/commands/feedback.ts +219 -0
- package/src/commands/init.ts +26 -0
- package/src/commands/launch.e2e.test.ts +9 -1
- package/src/commands/launch.ts +174 -11
- package/src/commands/lint-skill.ts +157 -0
- package/src/commands/marketplace.ts +763 -2
- package/src/commands/new.ts +1 -1
- package/src/commands/optimizer.ts +92 -28
- package/src/commands/profile-draft-skill.test.ts +96 -0
- package/src/commands/profile-draft-skill.ts +287 -0
- package/src/commands/profile-evolve.test.ts +126 -0
- package/src/commands/profile-evolve.ts +0 -0
- package/src/commands/profile-suggest.ts +223 -0
- package/src/commands/profile.ts +41 -0
- package/src/commands/quick.ts +2 -17
- package/src/commands/scan.ts +2 -2
- package/src/commands/score.ts +1 -1
- package/src/commands/share.ts +1 -1
- package/src/commands/sources.ts +2 -2
- package/src/commands/submit-profile.ts +262 -0
- package/src/commands/upgrade.ts +1 -1
- package/src/commands/use.ts +35 -5
- package/src/commands/validate.ts +1 -1
- package/src/index.ts +66 -0
- package/src/lib/analytics.ts +48 -2
- package/src/lib/claude-binary.ts +39 -0
- package/src/lib/cli-extractor.ts +77 -0
- package/src/lib/cluster-skills.test.ts +268 -0
- package/src/lib/cluster-skills.ts +290 -0
- package/src/lib/credentials-sync.test.ts +208 -0
- package/src/lib/credentials-sync.ts +205 -0
- package/src/lib/mcp-materializer.test.ts +1 -1
- package/src/lib/persona-playbooks.test.ts +111 -0
- package/src/lib/pr-poster.test.ts +243 -0
- package/src/lib/pr-poster.ts +285 -0
- package/src/lib/pr-throttle.test.ts +148 -0
- package/src/lib/pr-throttle.ts +209 -0
- package/src/lib/profile-generator.test.ts +1 -1
- package/src/lib/profile-generator.ts +2 -2
- package/src/lib/profile-linter.test.ts +6 -3
- package/src/lib/profile-linter.ts +71 -8
- package/src/lib/profile-loader.test.ts +1 -1
- package/src/lib/profile-loader.ts +16 -0
- package/src/lib/resolver-local.test.ts +1 -1
- package/src/lib/resolver-npx.test.ts +76 -1
- package/src/lib/resolver-npx.ts +35 -3
- package/src/lib/resolver-plugins.test.ts +1 -1
- package/src/lib/runtime-materializer.test.ts +191 -7
- package/src/lib/runtime-materializer.ts +310 -42
- package/src/lib/scan-plugins.test.ts +1 -1
- package/src/lib/skill-linter.test.ts +174 -0
- package/src/lib/skill-linter.ts +507 -0
- package/src/lib/skill-subset.test.ts +95 -0
- package/src/lib/skill-subset.ts +166 -0
- package/src/lib/star-prompt.ts +1 -1
- package/src/lib/uvx-installer.test.ts +229 -0
- package/src/lib/uvx-installer.ts +278 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `cue eval-behavior` — purely structural, no LLM or live commands.
|
|
3
|
+
* Uses the real profile tree because the loader is happy to find existing
|
|
4
|
+
* scenarios under resources/evals/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test, beforeEach } from "bun:test";
|
|
8
|
+
import { run as evalRun } from "./eval-behavior";
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Some sibling tests redirect CUE_PROFILES_DIR — restore so loadProfile finds real profiles.
|
|
12
|
+
delete process.env.CUE_PROFILES_DIR;
|
|
13
|
+
delete process.env.SOUL_PROFILES_DIR;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
async function capture<T>(fn: () => Promise<T>): Promise<{ stdout: string; value: T }> {
|
|
17
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
18
|
+
let buf = "";
|
|
19
|
+
(process.stdout as any).write = (c: string | Uint8Array) => { buf += String(c); return true; };
|
|
20
|
+
try { const value = await fn(); return { stdout: buf, value }; }
|
|
21
|
+
finally { (process.stdout as any).write = orig; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("cue eval-behavior", () => {
|
|
25
|
+
test("core passes both seeded scenarios", async () => {
|
|
26
|
+
const { stdout, value } = await capture(() => evalRun(["core", "--json"]));
|
|
27
|
+
expect(value).toBe(0);
|
|
28
|
+
const reports = JSON.parse(stdout) as Array<{ profile: string; results: Array<{ scenario: string; passed: boolean; score: number; max: number }> }>;
|
|
29
|
+
expect(reports).toHaveLength(1);
|
|
30
|
+
expect(reports[0]!.profile).toBe("core");
|
|
31
|
+
expect(reports[0]!.results.length).toBeGreaterThan(0);
|
|
32
|
+
for (const r of reports[0]!.results) {
|
|
33
|
+
expect(r.passed).toBe(true);
|
|
34
|
+
expect(r.score).toBeGreaterThan(0);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("explicit profile arg evaluates that profile and reports its inherited scenarios", async () => {
|
|
39
|
+
// Every profile inheriting core now picks up the seeded evals.
|
|
40
|
+
const { stdout, value } = await capture(() => evalRun(["frontend", "--json"]));
|
|
41
|
+
expect(value === 0 || value === 1).toBe(true);
|
|
42
|
+
const reports = JSON.parse(stdout) as Array<{ profile: string; results: Array<{ scenario: string }> }>;
|
|
43
|
+
expect(reports[0]!.profile).toBe("frontend");
|
|
44
|
+
// frontend inherits core, so the core-declared scenarios show up here too.
|
|
45
|
+
const scenarioNames = reports[0]!.results.map((r) => r.scenario);
|
|
46
|
+
expect(scenarioNames).toContain("feature-shipping");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("--all runs across every profile + JSON output is well-formed", async () => {
|
|
50
|
+
const { stdout, value } = await capture(() => evalRun(["--all", "--json"]));
|
|
51
|
+
expect(value === 0 || value === 1).toBe(true); // 1 if any profile fails
|
|
52
|
+
const reports = JSON.parse(stdout);
|
|
53
|
+
expect(Array.isArray(reports)).toBe(true);
|
|
54
|
+
expect(reports.length).toBeGreaterThan(5);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cue eval-behavior [profile]` — structural eval harness.
|
|
3
|
+
*
|
|
4
|
+
* Reads `resources/evals/<scenario>.md` files referenced from each profile's
|
|
5
|
+
* `evals:` field, parses each scenario's required + recommended capabilities,
|
|
6
|
+
* and scores the profile by whether it actually has those skills/commands/
|
|
7
|
+
* playbooks/gates loaded.
|
|
8
|
+
*
|
|
9
|
+
* This is a STRUCTURAL eval — it doesn't run an LLM. It answers "is this
|
|
10
|
+
* profile equipped to handle scenario X?", not "did it do well at X?". That's
|
|
11
|
+
* Phase 5 territory and needs LLM-in-the-loop.
|
|
12
|
+
*
|
|
13
|
+
* Output: per-scenario pass/fail + a single summary score per profile.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
17
|
+
import { join, resolve, dirname, isAbsolute } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
|
|
21
|
+
import { loadProfile, listProfiles } from "../lib/profile-loader";
|
|
22
|
+
import { resolveProfileForCwd } from "../lib/cwd-resolver";
|
|
23
|
+
import type { ResolvedProfile } from "../../profiles/_types";
|
|
24
|
+
|
|
25
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
26
|
+
const EVALS_ROOT = join(REPO_ROOT, "resources", "evals");
|
|
27
|
+
|
|
28
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
|
|
29
|
+
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
|
|
30
|
+
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
|
|
31
|
+
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
|
|
32
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
33
|
+
|
|
34
|
+
interface EvalScenario {
|
|
35
|
+
name: string;
|
|
36
|
+
description: string;
|
|
37
|
+
requiredSkills: string[];
|
|
38
|
+
oneOfCommands: string[]; // any one satisfies
|
|
39
|
+
recommendedPlaybooks: string[];
|
|
40
|
+
recommendedGates: string[];
|
|
41
|
+
triggerPhrases: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseScenario(path: string): EvalScenario | null {
|
|
45
|
+
let raw: string;
|
|
46
|
+
try { raw = readFileSync(path, "utf8"); } catch { return null; }
|
|
47
|
+
const fm = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
48
|
+
const name = fm?.[1]?.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? path;
|
|
49
|
+
const description = fm?.[1]?.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
|
|
50
|
+
|
|
51
|
+
const section = (heading: string): string[] => {
|
|
52
|
+
const re = new RegExp(`^##\\s+${heading}[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s+|$)`, "m");
|
|
53
|
+
const m = raw.match(re);
|
|
54
|
+
if (!m) return [];
|
|
55
|
+
return m[1]!.split("\n")
|
|
56
|
+
.filter((l) => l.startsWith("- "))
|
|
57
|
+
.map((l) => l.slice(2).split("#")[0]!.trim())
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
name,
|
|
63
|
+
description,
|
|
64
|
+
requiredSkills: section("Skills"),
|
|
65
|
+
oneOfCommands: section("Commands"),
|
|
66
|
+
recommendedPlaybooks: section("Playbooks"),
|
|
67
|
+
recommendedGates: section("Quality gates"),
|
|
68
|
+
triggerPhrases: section("Trigger phrases"),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ScenarioResult {
|
|
73
|
+
scenario: string;
|
|
74
|
+
score: number;
|
|
75
|
+
max: number;
|
|
76
|
+
passed: boolean;
|
|
77
|
+
missing: string[]; // human-readable list of what's lacking
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function scoreScenario(profile: ResolvedProfile, scenario: EvalScenario): ScenarioResult {
|
|
81
|
+
const skillSlugs = new Set(profile.skills.local.map((s) => s.id));
|
|
82
|
+
const commandSet = new Set((profile.commands ?? []).map((c) => c.replace(/\.md$/, "")));
|
|
83
|
+
const playbookSet = new Set(((profile as any).playbooks ?? []).map((p: string) => p.replace(/\.md$/, "")));
|
|
84
|
+
const gateSet = new Set(((profile as any).qualityGates ?? []));
|
|
85
|
+
|
|
86
|
+
let score = 0;
|
|
87
|
+
let max = 0;
|
|
88
|
+
const missing: string[] = [];
|
|
89
|
+
|
|
90
|
+
for (const req of scenario.requiredSkills) {
|
|
91
|
+
max += 1;
|
|
92
|
+
if (skillSlugs.has(req)) score += 1;
|
|
93
|
+
else missing.push(`skill: ${req}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (scenario.oneOfCommands.length > 0) {
|
|
97
|
+
max += 1;
|
|
98
|
+
if (scenario.oneOfCommands.some((c) => commandSet.has(c))) score += 1;
|
|
99
|
+
else missing.push(`commands: need any of [${scenario.oneOfCommands.join(", ")}]`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const pb of scenario.recommendedPlaybooks) {
|
|
103
|
+
max += 1;
|
|
104
|
+
if (playbookSet.has(pb)) score += 1;
|
|
105
|
+
else missing.push(`playbook: ${pb}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const gate of scenario.recommendedGates) {
|
|
109
|
+
max += 1;
|
|
110
|
+
if (gateSet.has(gate)) score += 1;
|
|
111
|
+
else missing.push(`quality-gate: ${gate}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Pass at >= 50%. If max is 0 (scenario declared nothing), trivially passes.
|
|
115
|
+
const passed = max === 0 || score >= Math.ceil(max / 2);
|
|
116
|
+
return { scenario: scenario.name, score, max, passed, missing };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function evalProfile(name: string): Promise<{ profile: string; results: ScenarioResult[] }> {
|
|
120
|
+
const profile = await loadProfile(name);
|
|
121
|
+
const evalRefs = ((profile as any).evals ?? []) as string[];
|
|
122
|
+
const results: ScenarioResult[] = [];
|
|
123
|
+
for (const ref of evalRefs) {
|
|
124
|
+
const path = isAbsolute(ref) ? ref : join(EVALS_ROOT, ref.endsWith(".md") ? ref : `${ref}.md`);
|
|
125
|
+
const scenario = parseScenario(path);
|
|
126
|
+
if (!scenario) {
|
|
127
|
+
results.push({ scenario: ref, score: 0, max: 0, passed: false, missing: [`scenario file not found: ${path}`] });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
results.push(scoreScenario(profile, scenario));
|
|
131
|
+
}
|
|
132
|
+
return { profile: name, results };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function run(args: string[]): Promise<number> {
|
|
136
|
+
const asJson = args.includes("--json");
|
|
137
|
+
const all = args.includes("--all");
|
|
138
|
+
|
|
139
|
+
let profiles: string[];
|
|
140
|
+
if (all) {
|
|
141
|
+
profiles = await listProfiles();
|
|
142
|
+
} else {
|
|
143
|
+
const explicit = args.find((a) => !a.startsWith("-"));
|
|
144
|
+
if (explicit) profiles = [explicit];
|
|
145
|
+
else {
|
|
146
|
+
try {
|
|
147
|
+
const r = await resolveProfileForCwd({ cwd: process.cwd(), homeDir: homedir(), configDir: join(homedir(), ".config", "cue") });
|
|
148
|
+
if (r.source === "none") {
|
|
149
|
+
process.stderr.write("Usage: cue eval-behavior [profile] | --all\n");
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
profiles = [(r as any).profile];
|
|
153
|
+
} catch {
|
|
154
|
+
process.stderr.write("Usage: cue eval-behavior [profile] | --all\n");
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const reports = await Promise.all(profiles.map(evalProfile));
|
|
161
|
+
|
|
162
|
+
if (asJson) {
|
|
163
|
+
process.stdout.write(JSON.stringify(reports, null, 2) + "\n");
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let anyFail = 0;
|
|
168
|
+
for (const r of reports) {
|
|
169
|
+
if (r.results.length === 0) {
|
|
170
|
+
process.stdout.write(`\n ${dim("·")} ${r.profile} ${dim("(no evals declared)")}\n`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const passed = r.results.filter((s) => s.passed).length;
|
|
174
|
+
const total = r.results.length;
|
|
175
|
+
const tag = passed === total ? green(`PASS ${passed}/${total}`) : red(`FAIL ${passed}/${total}`);
|
|
176
|
+
process.stdout.write(`\n ${bold(r.profile)} ${tag}\n`);
|
|
177
|
+
for (const sc of r.results) {
|
|
178
|
+
const mark = sc.passed ? green("✓") : red("✗");
|
|
179
|
+
const pct = sc.max > 0 ? `(${sc.score}/${sc.max})` : "(no checks)";
|
|
180
|
+
process.stdout.write(` ${mark} ${sc.scenario.padEnd(20)} ${dim(pct)}\n`);
|
|
181
|
+
if (!sc.passed && sc.missing.length > 0) {
|
|
182
|
+
for (const m of sc.missing.slice(0, 5)) process.stdout.write(` ${yellow("→")} ${m}\n`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (passed < total) anyFail++;
|
|
186
|
+
}
|
|
187
|
+
process.stdout.write("\n");
|
|
188
|
+
return anyFail > 0 ? 1 : 0;
|
|
189
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `cue eval`.
|
|
3
|
+
*
|
|
4
|
+
* Runs the command's JSON mode against the real profiles tree (the repo's
|
|
5
|
+
* actual `profiles/`) and asserts on the structure + math. Avoids parsing
|
|
6
|
+
* ANSI-formatted text mode, which is unstable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test, beforeEach } from "bun:test";
|
|
10
|
+
import { run as evalRun } from "./eval";
|
|
11
|
+
|
|
12
|
+
// Some sibling test files set CUE_PROFILES_DIR to a temp dir and don't reset.
|
|
13
|
+
// Force the repo's real profiles/ tree before each test so loadProfile finds core/ecc/etc.
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
delete process.env.CUE_PROFILES_DIR;
|
|
16
|
+
delete process.env.SOUL_PROFILES_DIR;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
interface JsonReport {
|
|
20
|
+
profile: string;
|
|
21
|
+
counts: { skills: number; rules: number; commands: number; hooks: number; mcps: number; plugins: number };
|
|
22
|
+
tokens: {
|
|
23
|
+
perMessage: number;
|
|
24
|
+
onDemand: number;
|
|
25
|
+
bySource: Record<string, { perMessage: number; onDemand: number }>;
|
|
26
|
+
};
|
|
27
|
+
fullPerMessage: number;
|
|
28
|
+
savingsPct: number;
|
|
29
|
+
costPerMessage: string;
|
|
30
|
+
sessions: number;
|
|
31
|
+
score: number;
|
|
32
|
+
grade: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function captureStdout<T>(fn: () => Promise<T>): Promise<{ stdout: string; value: T }> {
|
|
36
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
37
|
+
let buf = "";
|
|
38
|
+
(process.stdout as any).write = (chunk: string | Uint8Array) => { buf += String(chunk); return true; };
|
|
39
|
+
try {
|
|
40
|
+
const value = await fn();
|
|
41
|
+
return { stdout: buf, value };
|
|
42
|
+
} finally {
|
|
43
|
+
(process.stdout as any).write = orig;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("cue eval", () => {
|
|
48
|
+
test("--json on a real profile returns the expected shape and positive totals", async () => {
|
|
49
|
+
const { stdout, value } = await captureStdout(() => evalRun(["core", "--json"]));
|
|
50
|
+
expect(value).toBe(0);
|
|
51
|
+
const report = JSON.parse(stdout) as JsonReport;
|
|
52
|
+
expect(report.profile).toBe("core");
|
|
53
|
+
expect(report.counts.skills).toBeGreaterThan(0);
|
|
54
|
+
expect(report.tokens.perMessage).toBeGreaterThan(0);
|
|
55
|
+
expect(report.tokens.perMessage).toBeLessThan(report.tokens.onDemand);
|
|
56
|
+
// sum of bySource.perMessage should equal tokens.perMessage
|
|
57
|
+
const sum = Object.values(report.tokens.bySource).reduce((s, b) => s + b.perMessage, 0);
|
|
58
|
+
expect(sum).toBe(report.tokens.perMessage);
|
|
59
|
+
expect(report.grade).toMatch(/^[A-F]$/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("--compare emits delta and is symmetric in absolute value", async () => {
|
|
63
|
+
const { stdout, value } = await captureStdout(() => evalRun(["--compare", "core", "ecc", "--json"]));
|
|
64
|
+
expect(value).toBe(0);
|
|
65
|
+
const cmp = JSON.parse(stdout) as { a: any; b: any; delta: { perMessage: number; score: number } };
|
|
66
|
+
expect(cmp.a.profile).toBe("core");
|
|
67
|
+
expect(cmp.b.profile).toBe("ecc");
|
|
68
|
+
expect(cmp.delta.perMessage).toBe(cmp.b.tokens.perMessage - cmp.a.tokens.perMessage);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("--all returns an array sorted by perMessage ascending", async () => {
|
|
72
|
+
const { stdout, value } = await captureStdout(() => evalRun(["--all", "--json"]));
|
|
73
|
+
expect(value).toBe(0);
|
|
74
|
+
const rows = JSON.parse(stdout) as Array<{ name: string; perMessage: number; ok: boolean }>;
|
|
75
|
+
expect(rows.length).toBeGreaterThan(5);
|
|
76
|
+
const okRows = rows.filter((r) => r.ok);
|
|
77
|
+
for (let i = 1; i < okRows.length; i++) {
|
|
78
|
+
expect(okRows[i].perMessage).toBeGreaterThanOrEqual(okRows[i - 1].perMessage);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("--compare with missing second arg returns usage error (exit 1)", async () => {
|
|
83
|
+
const orig = process.stderr.write.bind(process.stderr);
|
|
84
|
+
let err = "";
|
|
85
|
+
(process.stderr as any).write = (chunk: string | Uint8Array) => { err += String(chunk); return true; };
|
|
86
|
+
try {
|
|
87
|
+
const exit = await evalRun(["--compare", "core"]);
|
|
88
|
+
expect(exit).toBe(1);
|
|
89
|
+
expect(err).toContain("--compare");
|
|
90
|
+
} finally {
|
|
91
|
+
(process.stderr as any).write = orig;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("on-demand bodies exceed per-message tokens — verifies the lazy/eager split", async () => {
|
|
96
|
+
const { stdout } = await captureStdout(() => evalRun(["ecc", "--json"]));
|
|
97
|
+
const report = JSON.parse(stdout) as JsonReport;
|
|
98
|
+
// ecc declares rules + commands; both should have measurable on-demand cost
|
|
99
|
+
expect(report.tokens.bySource.rules.onDemand).toBeGreaterThan(report.tokens.bySource.rules.perMessage);
|
|
100
|
+
expect(report.tokens.bySource.commands.onDemand).toBeGreaterThan(report.tokens.bySource.commands.perMessage);
|
|
101
|
+
});
|
|
102
|
+
});
|