spawn/packages/cli/src/__tests__/check-entity.test.ts
Ahmed Abushagur 24a3c7328d
feat: show cloud prices as lead indicator (#2347)
* feat: show cloud prices as lead indicator, default OpenClaw to Kimi K2.5

- Add `price` field to all clouds in manifest.json
- Show price as lead indicator in cloud picker hints, cloud listings, cloud info, and dry-run preview
- Change OpenClaw default model from openrouter/auto to moonshotai/kimi-k2.5 (top used model by OpenClaw users)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add defensive guards for undefined cloud price in cached manifests

When users upgrade CLI but have cached manifests from before the price
field was added, c.price is undefined. Add ?? "" fallbacks and an
if-guard to prevent runtime crashes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
2026-03-08 23:41:39 -07:00

400 lines
14 KiB
TypeScript

import type { Manifest } from "../manifest";
import { beforeEach, describe, expect, it } from "bun:test";
import { checkEntity } from "../commands/index.js";
/**
* Tests for checkEntity (commands/shared.ts).
*
* checkEntity validates that a user-provided value exists in the manifest
* as the expected entity kind (agent or cloud). It returns true if valid,
* false otherwise. On failure it outputs error messages via @clack/prompts.
*
* Error branches:
* 1. Wrong-type detection: user typed a cloud name where an agent was expected
* (or vice versa) -- returns false with specific guidance.
* 2. Fuzzy match suggestion: user typed a close typo -- returns false with
* "Did you mean X?" suggestion.
* 3. Generic error: no close match found -- returns false with list command hint.
*
* This function is called in cmdRun (commands/run.ts) for both agent
* and cloud validation, making it critical for the run pipeline.
*/
// ── Test Fixtures ──────────────────────────────────────────────────────────
function createTestManifest(): Manifest {
return {
agents: {
claude: {
name: "Claude Code",
description: "AI coding assistant",
url: "https://claude.ai",
install: "npm install -g claude",
launch: "claude",
env: {
ANTHROPIC_API_KEY: "test",
},
},
codex: {
name: "Codex",
description: "AI pair programmer",
url: "https://codex.dev",
install: "npm install -g codex",
launch: "codex",
env: {
OPENAI_API_KEY: "test",
},
},
cline: {
name: "Cline",
description: "AI developer agent",
url: "https://cline.dev",
install: "npm install -g cline",
launch: "cline",
env: {},
},
},
clouds: {
sprite: {
name: "Sprite",
description: "Lightweight VMs",
price: "test",
url: "https://sprite.sh",
type: "vm",
auth: "SPRITE_TOKEN",
provision_method: "api",
exec_method: "ssh",
interactive_method: "ssh",
},
hetzner: {
name: "Hetzner Cloud",
description: "European cloud provider",
price: "test",
url: "https://hetzner.com",
type: "cloud",
auth: "HCLOUD_TOKEN",
provision_method: "api",
exec_method: "ssh",
interactive_method: "ssh",
},
vultr: {
name: "Vultr",
description: "Cloud compute",
price: "test",
url: "https://vultr.com",
type: "cloud",
auth: "VULTR_API_KEY",
provision_method: "api",
exec_method: "ssh",
interactive_method: "ssh",
},
},
matrix: {
"sprite/claude": "implemented",
"sprite/codex": "implemented",
"sprite/cline": "missing",
"hetzner/claude": "implemented",
"hetzner/codex": "missing",
"hetzner/cline": "missing",
"vultr/claude": "implemented",
"vultr/codex": "missing",
"vultr/cline": "missing",
},
};
}
// ── Tests ───────────────────────────────────────────────────────────────────
let manifest: Manifest;
describe("checkEntity", () => {
beforeEach(() => {
manifest = createTestManifest();
});
// ── Non-existent entities: no close match (distance > 3) ───────────────
describe("non-existent entities with no close match", () => {
it("should return false for completely unknown agent 'kubernetes'", () => {
expect(checkEntity(manifest, "kubernetes", "agent")).toBe(false);
});
it("should return false for completely unknown cloud 'amazonaws'", () => {
expect(checkEntity(manifest, "amazonaws", "cloud")).toBe(false);
});
it("should return false for unknown agent 'terraform'", () => {
expect(checkEntity(manifest, "terraform", "agent")).toBe(false);
});
it("should return false for unknown cloud 'googlecloud'", () => {
expect(checkEntity(manifest, "googlecloud", "cloud")).toBe(false);
});
it("should return false for strings far from any candidate", () => {
expect(checkEntity(manifest, "zzzzzzz", "agent")).toBe(false);
expect(checkEntity(manifest, "zzzzzzz", "cloud")).toBe(false);
});
});
// ── Fuzzy match: close typos that should return false ──────────────────
describe("fuzzy match for close typos", () => {
it("should return false for 'claud' (typo of claude, distance 1)", () => {
expect(checkEntity(manifest, "claud", "agent")).toBe(false);
});
it("should return false for 'claudee' (typo of claude, distance 1)", () => {
expect(checkEntity(manifest, "claudee", "agent")).toBe(false);
});
it("should return false for 'codx' (typo of codex, distance 1)", () => {
expect(checkEntity(manifest, "codx", "agent")).toBe(false);
});
it("should return false for 'codexs' (typo of codex, distance 1)", () => {
expect(checkEntity(manifest, "codexs", "agent")).toBe(false);
});
it("should return false for 'clin' (typo of cline, distance 1)", () => {
expect(checkEntity(manifest, "clin", "agent")).toBe(false);
});
it("should return false for 'sprit' (typo of sprite, distance 1)", () => {
expect(checkEntity(manifest, "sprit", "cloud")).toBe(false);
});
it("should return false for 'spritee' (typo of sprite, distance 1)", () => {
expect(checkEntity(manifest, "spritee", "cloud")).toBe(false);
});
it("should return false for 'hetzne' (typo of hetzner, distance 1)", () => {
expect(checkEntity(manifest, "hetzne", "cloud")).toBe(false);
});
it("should return false for 'vulr' (typo of vultr, distance 1)", () => {
expect(checkEntity(manifest, "vulr", "cloud")).toBe(false);
});
it("should return false for 'vultrr' (typo of vultr, distance 1)", () => {
expect(checkEntity(manifest, "vultrr", "cloud")).toBe(false);
});
it("should return false for multi-character distance typos", () => {
// "claue" has distance 2 from "claude" — still within threshold 3
expect(checkEntity(manifest, "claue", "agent")).toBe(false);
// "sprt" has distance 2 from "sprite"
expect(checkEntity(manifest, "sprt", "cloud")).toBe(false);
});
});
// ── Empty and boundary inputs ──────────────────────────────────────────
describe("empty and boundary inputs", () => {
it("should return false for empty string as agent", () => {
expect(checkEntity(manifest, "", "agent")).toBe(false);
});
it("should return false for empty string as cloud", () => {
expect(checkEntity(manifest, "", "cloud")).toBe(false);
});
it("should handle single character input without crashing", () => {
expect(checkEntity(manifest, "a", "agent")).toBe(false);
});
it("should handle single character input for cloud without crashing", () => {
expect(checkEntity(manifest, "x", "cloud")).toBe(false);
});
it("should handle very long input without crashing", () => {
const longInput = "a".repeat(100);
expect(checkEntity(manifest, longInput, "agent")).toBe(false);
});
it("should handle input with special characters", () => {
expect(checkEntity(manifest, "claude-code", "agent")).toBe(false);
});
it("should handle input with underscores", () => {
expect(checkEntity(manifest, "open_gptme", "agent")).toBe(false);
});
it("should handle numeric input", () => {
expect(checkEntity(manifest, "123", "agent")).toBe(false);
});
});
// ── Edge cases with minimal manifest ───────────────────────────────────
describe("minimal manifest edge cases", () => {
it("should return false when agents collection is empty", () => {
const emptyAgents: Manifest = {
agents: {},
clouds: {
sprite: manifest.clouds.sprite,
},
matrix: {},
};
expect(checkEntity(emptyAgents, "claude", "agent")).toBe(false);
});
it("should return false when clouds collection is empty", () => {
const emptyClouds: Manifest = {
agents: {
claude: manifest.agents.claude,
},
clouds: {},
matrix: {},
};
expect(checkEntity(emptyClouds, "sprite", "cloud")).toBe(false);
});
it("should not crash on completely empty manifest (agent check)", () => {
const empty: Manifest = {
agents: {},
clouds: {},
matrix: {},
};
expect(checkEntity(empty, "test", "agent")).toBe(false);
});
it("should not crash on completely empty manifest (cloud check)", () => {
const empty: Manifest = {
agents: {},
clouds: {},
matrix: {},
};
expect(checkEntity(empty, "test", "cloud")).toBe(false);
});
it("should detect wrong type with single-entry collections", () => {
const single: Manifest = {
agents: {
claude: manifest.agents.claude,
},
clouds: {
sprite: manifest.clouds.sprite,
},
matrix: {},
};
// "sprite" exists in clouds but not agents
expect(checkEntity(single, "sprite", "agent")).toBe(false);
// "claude" exists in agents but not clouds
expect(checkEntity(single, "claude", "cloud")).toBe(false);
});
});
// ── All agents are valid when checked as agents ────────────────────────
describe("all manifest agents validate correctly", () => {
it("should validate every agent in the manifest", () => {
const agentKeys = Object.keys(manifest.agents);
for (const key of agentKeys) {
expect(checkEntity(manifest, key, "agent")).toBe(true);
}
});
it("should reject every agent key when checked as cloud", () => {
const agentKeys = Object.keys(manifest.agents);
for (const key of agentKeys) {
expect(checkEntity(manifest, key, "cloud")).toBe(false);
}
});
});
// ── All clouds are valid when checked as clouds ────────────────────────
describe("all manifest clouds validate correctly", () => {
it("should validate every cloud in the manifest", () => {
const cloudKeys = Object.keys(manifest.clouds);
for (const key of cloudKeys) {
expect(checkEntity(manifest, key, "cloud")).toBe(true);
}
});
it("should reject every cloud key when checked as agent", () => {
const cloudKeys = Object.keys(manifest.clouds);
for (const key of cloudKeys) {
expect(checkEntity(manifest, key, "agent")).toBe(false);
}
});
});
// ── Cross-kind fuzzy match: detect swapped args with typos ──────────
describe("cross-kind fuzzy match for swapped args with typos", () => {
it("should return false for 'htzner' as agent (close to cloud 'hetzner')", () => {
expect(checkEntity(manifest, "htzner", "agent")).toBe(false);
});
it("should return false for 'sprit' as agent (close to cloud 'sprite')", () => {
expect(checkEntity(manifest, "sprit", "agent")).toBe(false);
});
it("should return false for 'vulr' as agent (close to cloud 'vultr')", () => {
expect(checkEntity(manifest, "vulr", "agent")).toBe(false);
});
it("should return false for 'claud' as cloud (close to agent 'claude')", () => {
expect(checkEntity(manifest, "claud", "cloud")).toBe(false);
});
it("should return false for 'codx' as cloud (close to agent 'codex')", () => {
expect(checkEntity(manifest, "codx", "cloud")).toBe(false);
});
it("should return false for 'clin' as cloud (close to agent 'cline')", () => {
expect(checkEntity(manifest, "clin", "cloud")).toBe(false);
});
it("should prefer same-kind match over cross-kind match", () => {
// "cline" checked as agent should match exactly (same-kind), not cross-kind
expect(checkEntity(manifest, "cline", "agent")).toBe(true);
});
it("should not suggest cross-kind match for values far from any candidate", () => {
// "zzzzzzz" is far from all agent and cloud names
expect(checkEntity(manifest, "zzzzzzz", "agent")).toBe(false);
expect(checkEntity(manifest, "zzzzzzz", "cloud")).toBe(false);
});
});
// ── Manifest with overlapping key names ────────────────────────────────
describe("manifest with overlapping patterns", () => {
it("should handle agent and cloud with similar names", () => {
const overlapping: Manifest = {
agents: {
local: {
name: "Local Agent",
description: "Local agent",
url: "",
install: "",
launch: "",
env: {},
},
},
clouds: {
"local-cloud": {
name: "Local Cloud",
description: "Local cloud provider",
price: "test",
url: "",
type: "local",
auth: "none",
provision_method: "local",
exec_method: "local",
interactive_method: "local",
},
},
matrix: {},
};
expect(checkEntity(overlapping, "local", "agent")).toBe(true);
expect(checkEntity(overlapping, "local-cloud", "cloud")).toBe(true);
expect(checkEntity(overlapping, "local", "cloud")).toBe(false);
expect(checkEntity(overlapping, "local-cloud", "agent")).toBe(false);
});
});
});