evOo-local/apps/mobile/lib/endpoint.test.ts
cynarAI 2c6b5c3d7b
chore: remove legacy orchestrator, trading and one-off leftovers (#109)
* chore: remove legacy orchestrator, trading and one-off script leftovers

The repository still carried the pre-evOo prototype: a Python orchestrator,
the old HTML "evoo-platform" cockpit (whose test ran in the root unit
suite), an apex-trading package, the hot-reload deploy.sh for it, a
permissions manifest, an architecture note with local file links, agent
handoff notes, a Steve Jobs persona template and about ninety one-off
scripts that probe a developer database with hard-coded local URLs. None
of it is used by the product, CI or the real deploy in ops/evo-deploy.

Delete those files, drop the evoo-platform test glob from the root vitest
config, the Cockpit section from .env.example and the dead biome and
gitignore entries, and update the docs that listed the removed tools.
scripts/start-tmux.sh no longer starts the removed sentinel and resolves
the checkout from its own location instead of a machine-specific path;
scripts/seed-ollama.ts requires DATABASE_URL instead of falling back to a
local URL with a username.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* fix(web): drop trading and persona prompts from the conversation shell

The web shell still showed leftovers from the trading prototype: composer
chips with German trading and "Steve Jobs" prompts, trading suggestion
chips under the empty conversation line, a German trading hero for the
empty transcript, and a German "data protection" status panel in the
sidebar. They suggested actions the product does not have and added
persistent chrome.

Remove the composer chips and their helper, the empty-state chips and the
hero (the empty conversation keeps its existing one-line start hint), the
sidebar trading indicator and the data trust panel. The new-bot E2E now
asserts the plain empty state; the sidebar spec no longer expects the
trading status, and the placeholder Steve Jobs spec with no assertions is
gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* refactor: remove the trading bot status integration

With the sidebar trading indicator gone, the only remaining consumer of
the trading bot status was a banner in General settings that reused the
same component. evOo is a personal assistant; a status proxy and poller
for one external trading service, configured through service-specific
TRADING_BOT_URL and VITE_TRADING_BOT_URL variables, does not belong in
the core product.

Remove the settings banner and its prop plumbing, the web poller, the
indicator component, the /api/trading-bot/status proxy, the shared core
contract, the env loading and both variables from .env.example, together
with their tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* chore: remove one-off database scripts and a stale review note

packages/db carried four hand-run scripts outside the package build that
query or seed a developer database with a hard-coded connection string,
one of them for a specific account. docs/VOICE-FIRST-REVIEW.md was a
one-time verification note for a workflow that no longer exists and
named private machines and local paths. Nothing references them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* chore: replace machine-specific paths and addresses with placeholders

ORIGIN.md pinned the intended checkout to an absolute path under a
personal home directory, docs/LOCAL-SETUP.md listed folders on one
developer's machine, and two tests used what looks like a real tailnet
address. Use a home-relative path, drop the machine-specific folder list,
and use 100.100.100.100 in the tests; it is still inside 100.64.0.0/10,
so both tests check the same thing as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

* test(web): drop the trading status from the shell contrast test

main's contrast test now waits for the sidebar trading status before
measuring; this branch removes that status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-04 01:13:52 +02:00

180 lines
6.3 KiB
TypeScript

import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
API_PROBE_TIMEOUT_MS,
apiBaseWarning,
defaultApiBase,
displayApiHost,
MAX_API_PROBE_RESPONSE_BYTES,
normalizeApiBase,
probeApiBase,
usesCustomApiBase,
} from "./endpoint.js";
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
describe("normalizeApiBase", () => {
it("trims, adds https, and keeps only the origin", () => {
expect(normalizeApiBase(" app.example.com/rpc ")).toEqual({
ok: true,
url: "https://app.example.com",
});
expect(normalizeApiBase("https://rakazo.example.com:8443/api/")).toEqual({
ok: true,
url: "https://rakazo.example.com:8443",
});
expect(normalizeApiBase("http://192.168.1.20:3100/")).toEqual({
ok: true,
url: "http://192.168.1.20:3100",
});
expect(normalizeApiBase("http://app.example.com")).toEqual({
ok: false,
error: "Public servers need https://",
});
});
it("rejects empty, non-http, and malformed values", () => {
expect(normalizeApiBase("")).toMatchObject({ ok: false });
expect(normalizeApiBase(" ")).toMatchObject({ ok: false });
expect(normalizeApiBase("javascript:alert(1)")).toMatchObject({ ok: false });
expect(normalizeApiBase("ftp://files.example.com")).toMatchObject({ ok: false });
expect(normalizeApiBase("http://")).toMatchObject({ ok: false });
});
it("strips credentials from the stored origin", () => {
expect(normalizeApiBase("https://user:pass@app.example.com/rpc")).toEqual({
ok: true,
url: "https://app.example.com",
});
});
});
describe("display and warnings", () => {
it("falls back to loopback when the compile-time endpoint is invalid", async () => {
vi.stubEnv("EXPO_PUBLIC_API_URL", "ftp://files.example.com");
vi.resetModules();
const endpoint = await import("./endpoint.js");
expect(endpoint.defaultApiBase()).toBe("http://127.0.0.1:3100");
vi.unstubAllEnvs();
});
it("falls back to loopback when the compile-time endpoint is public HTTP", async () => {
vi.stubEnv("EXPO_PUBLIC_API_URL", "http://app.example.com");
vi.resetModules();
const endpoint = await import("./endpoint.js");
expect(endpoint.defaultApiBase()).toBe("http://127.0.0.1:3100");
vi.unstubAllEnvs();
});
it("shows host and non-default port", () => {
expect(displayApiHost("https://rakazo.example.com")).toBe("rakazo.example.com");
expect(displayApiHost("http://10.0.0.8:3100")).toBe("10.0.0.8:3100");
});
it("warns on public http but not LAN or loopback", () => {
expect(apiBaseWarning("https://app.example.com")).toBeNull();
expect(apiBaseWarning("http://127.0.0.1:3100")).toBeNull();
expect(apiBaseWarning("http://192.168.1.20:3100")).toBeNull();
expect(apiBaseWarning("http://100.64.0.1:3100")).toBeNull();
expect(apiBaseWarning("http://100.100.100.100:3100")).toBeNull();
expect(apiBaseWarning("http://100.127.255.255:3100")).toBeNull();
expect(apiBaseWarning("http://app.example.com")).toMatch(/https/i);
});
it("treats the compile-time default as not custom", () => {
expect(usesCustomApiBase(defaultApiBase())).toBe(false);
expect(usesCustomApiBase("https://rakazo.example.com")).toBe(true);
});
});
describe("probeApiBase", () => {
it("accepts a Rakazo /rpc/health response", async () => {
const fetchImpl = vi.fn(
async () =>
new Response(JSON.stringify({ json: { ok: true, version: "0.1.0" } }), { status: 200 }),
) as unknown as typeof fetch;
await expect(probeApiBase("https://app.example.com", fetchImpl)).resolves.toEqual({
ok: true,
url: "https://app.example.com",
});
expect(fetchImpl).toHaveBeenCalledWith(
"https://app.example.com/rpc/health",
expect.objectContaining({ method: "POST" }),
);
});
it("rejects a host that is up but is not Rakazo", async () => {
const fetchImpl = vi.fn(
async () => new Response("ok", { status: 200 }),
) as unknown as typeof fetch;
await expect(probeApiBase("https://example.com", fetchImpl)).resolves.toMatchObject({
ok: false,
});
});
it("rejects an oversized health response", async () => {
const fetchImpl = vi.fn(
async () =>
new Response(JSON.stringify({ json: { ok: true } }), {
headers: { "content-length": String(MAX_API_PROBE_RESPONSE_BYTES + 1) },
}),
) as unknown as typeof fetch;
await expect(probeApiBase("https://app.example.com", fetchImpl)).resolves.toMatchObject({
ok: false,
error: "Could not reach that server",
});
});
it("returns a failure when an injected fetch ignores the probe signal", async () => {
vi.useFakeTimers();
const fetchImpl = vi.fn(
(_input: RequestInfo | URL, _init?: RequestInit) => new Promise<Response>(() => undefined),
);
const pending = probeApiBase("https://app.example.com", fetchImpl as typeof fetch);
await vi.advanceTimersByTimeAsync(API_PROBE_TIMEOUT_MS);
await expect(pending).resolves.toMatchObject({
ok: false,
error: "Could not reach that server",
});
expect(fetchImpl.mock.calls[0]?.[1]?.signal?.aborted).toBe(true);
});
it("returns a failure when a health response body never settles", async () => {
vi.useFakeTimers();
const cancel = vi.fn();
const fetchImpl = vi.fn(async () => new Response(new ReadableStream({ cancel })));
const pending = probeApiBase("https://app.example.com", fetchImpl as typeof fetch);
await vi.advanceTimersByTimeAsync(API_PROBE_TIMEOUT_MS);
await expect(pending).resolves.toMatchObject({
ok: false,
error: "Could not reach that server",
});
expect(cancel).toHaveBeenCalledOnce();
});
});
describe("mobile custom server UI", () => {
it("exposes a sign-in control that writes the stored origin", () => {
const dir = path.dirname(fileURLToPath(import.meta.url));
const signIn = readFileSync(path.join(dir, "../app/sign-in.tsx"), "utf8");
const api = readFileSync(path.join(dir, "api.ts"), "utf8");
expect(signIn).toContain("Use a custom server");
expect(signIn).toContain("saveApiBase");
expect(signIn).toContain("probeApiBase");
expect(api).toContain("currentApiBase()");
expect(api).not.toMatch(/export const API /);
});
});