File size: 5,102 Bytes
a42bc3b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import { describe, expect, it } from "vitest";
import { createComputeClient } from "@homepilot/compute-client";
import type { ApiClient } from "@homepilot/api-client";

/**
 * PR 3 — the compute-client admin surface. Verifies the wire seam: requests are
 * snake_case (recursively, for nested modality defaults), responses are
 * unwrapped + camelized, and DELETE routes hit the right paths.
 */

type Call = { method: string; path: string; body?: unknown };

function fakeApi(responses: Record<string, unknown>): { api: ApiClient; calls: Call[] } {
  const calls: Call[] = [];
  const reply = (path: string) => responses[path] ?? {};
  const api: ApiClient = {
    baseUrl: "http://test",
    get: async (p) => { calls.push({ method: "GET", path: p }); return reply(p) as never; },
    post: async (p, b) => { calls.push({ method: "POST", path: p, body: b }); return reply(p) as never; },
    put: async (p, b) => { calls.push({ method: "PUT", path: p, body: b }); return reply(p) as never; },
    del: async (p) => { calls.push({ method: "DELETE", path: p }); return {} as never; },
  };
  return { api, calls };
}

describe("compute-client admin", () => {
  it("lists and camelizes sources", async () => {
    const { api } = fakeApi({
      "/compute/admin/sources": { sources: [{ id: "s", name: "S", kind: "openai_compatible", base_url: "http://x", credential_ref: "r", enabled: true, execution_type: "direct" }] },
    });
    const sources = await createComputeClient(api).listComputeSources();
    expect(sources[0].baseUrl).toBe("http://x");
    expect(sources[0].credentialRef).toBe("r");
    expect(sources[0].executionType).toBe("direct");
  });

  it("sends per-model routes as snake_case", async () => {
    const { api, calls } = fakeApi({ "/compute/admin/routes": { route: {} } });
    await createComputeClient(api).upsertRoute({
      modality: "chat", modelId: "m", primaryTarget: "device:colab",
      fallbackTargets: ["local"], strategy: "pinned", privacy: "private_nodes",
    });
    const body = calls.find((c) => c.method === "POST")!.body as Record<string, unknown>;
    expect(body.model_id).toBe("m");
    expect(body.primary_target).toBe("device:colab");
    expect(body.fallback_targets).toEqual(["local"]);
  });

  it("recursively snake-cases nested modality defaults in settings", async () => {
    const { api, calls } = fakeApi({ "/compute/admin/settings": { settings: {} } });
    await createComputeClient(api).putComputeSettings({
      computeMode: "auto",
      modalityDefaults: {
        image: { modality: "image", strategy: "prefer_remote", fallbackTargets: ["local"], privacy: "private_nodes" },
      },
    });
    const body = calls.find((c) => c.method === "PUT")!.body as Record<string, unknown>;
    expect(body.compute_mode).toBe("auto");
    const md = body.modality_defaults as Record<string, Record<string, unknown>>;
    expect(md.image.fallback_targets).toEqual(["local"]);
  });

  it("resolveRoute passes model_id and returns the plan", async () => {
    const { api, calls } = fakeApi({
      "/compute/admin/resolve?modality=chat&model_id=m": { modality: "chat", model_id: "m", matched: "route", compute_mode: "local", targets: ["device:colab", "local"] },
    });
    const plan = await createComputeClient(api).resolveRoute("chat", "m");
    expect(plan.matched).toBe("route");
    expect(plan.targets).toEqual(["device:colab", "local"]);
    expect(calls[0].path).toContain("model_id=m");
  });

  it("tests a source and reads local resources", async () => {
    const { api, calls } = fakeApi({
      "/compute/admin/sources/oa/test": { ok: true, reason: "Reachable." },
      "/v1/system/resources": { gpus: [{ available: true, name: "RTX 4090", vram_total_mb: 24576, used_percent: 30 }], cpu: { percent: 12 }, ram: { percent: 40 } },
    });
    const client = createComputeClient(api);
    const t = await client.testComputeSource("oa");
    expect(t.ok).toBe(true);
    expect(calls[0]).toMatchObject({ method: "POST", path: "/compute/admin/sources/oa/test" });
    const res = await client.getLocalResources();
    expect(res.gpus[0].vramTotalMb).toBe(24576);   // snake→camel
    expect(res.gpus[0].usedPercent).toBe(30);
  });

  it("syncs from cloud via POST /compute/admin/sync", async () => {
    const { api, calls } = fakeApi({
      "/compute/admin/sync": { ok: true, linked: true, devices: 2, models: 5, source_id: "ollabridge-cloud" },
    });
    const r = await createComputeClient(api).syncFromCloud();
    expect(r).toMatchObject({ linked: true, devices: 2, models: 5 });
    expect(calls[0]).toMatchObject({ method: "POST", path: "/compute/admin/sync" });
  });

  it("deletes sources and routes at the right paths", async () => {
    const { api, calls } = fakeApi({});
    const client = createComputeClient(api);
    await client.deleteComputeSource("s 1");
    await client.deleteRoute("chat", "m/x");
    expect(calls[0]).toMatchObject({ method: "DELETE", path: "/compute/admin/sources/s%201" });
    expect(calls[1]).toMatchObject({ method: "DELETE", path: "/compute/admin/routes/chat/m%2Fx" });
  });
});