File size: 4,738 Bytes
2c440a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
106
107
108
109
110
111
112
113
114
115
/**
 * The message a person actually reads when vision fails.
 *
 * The strings below are the real ones from a real session, pasted rather than invented.
 */
import { describe, it, expect } from "vitest";
import { visionErrorMessage, parseVisionFailure } from "./visionError";

/** Exactly what the chat was handed when the local model's runner crashed. */
const RUNNER_STOPPED = new Error(
  'HTTP 422 Unprocessable Entity: {"ok":false,"error":"Ollama could not keep model \'moondream:latest\' running. It may exceed this computer\'s available RAM or VRAM. Select a smaller multimodal model or check the Ollama server logs.","error_code":"model_runner_stopped","analysis_text":"","meta":{"model":"moondream:latest","mode":"both"}}',
);

/** And when the model loaded but said nothing about a tall screenshot. */
const EMPTY = new Error(
  'HTTP 422 Unprocessable Entity: {"ok":false,"error":"moondream:latest returned no description of the image.","error_code":"empty_model_response","analysis_text":"","meta":{"model":"moondream:latest","mode":"both","image_size_bytes":278904}}',
);

describe("it stops dumping JSON at people", () => {
  it("shows the backend sentence, not the envelope", () => {
    const msg = visionErrorMessage(RUNNER_STOPPED);
    expect(msg).toContain("could not keep model");
    expect(msg).not.toContain("{");
    expect(msg).not.toContain("error_code");
    expect(msg).not.toContain("422");
  });

  it("and never shows the raw meta block", () => {
    expect(visionErrorMessage(EMPTY)).not.toContain("image_size_bytes");
  });
});

describe("it stops giving advice that is wrong", () => {
  it("does not tell someone to install the model they are looking at", () => {
    // This is the sentence that sent a user to check something already true: Moondream
    // was installed and visible in the model list while the chat told them to pull it.
    expect(visionErrorMessage(RUNNER_STOPPED)).not.toMatch(
      /ollama pull moondream/i,
    );
    expect(visionErrorMessage(EMPTY)).not.toMatch(
      /make sure a multimodal model is installed/i,
    );
  });

  it("gives the advice that matches what actually failed", () => {
    // "close some applications" appears only in the advice. An earlier version matched
    // "smaller multimodal model", which the backend's own sentence already contains — so
    // the test passed with the advice removed entirely, and a mutation proved it.
    expect(visionErrorMessage(RUNNER_STOPPED)).toMatch(
      /close some applications/i,
    );
    expect(visionErrorMessage(EMPTY)).toMatch(/larger multimodal model/i);
  });

  it("and says to install one only when none is installed", () => {
    const none = new Error(
      '{"ok":false,"error":"No multimodal model found.","error_code":"no_vision_model"}',
    );
    expect(visionErrorMessage(none)).toMatch(/ollama pull/i);
  });

  it("adds nothing when there is no code to go on", () => {
    // A guess about the cause is what caused this. Silence beats invention.
    const bare = new Error(
      '{"ok":false,"error":"Failed to connect to Ollama: timed out"}',
    );
    const msg = visionErrorMessage(bare);
    expect(msg).toBe("Failed to connect to Ollama: timed out.");
  });
});

describe("the shapes that are not a tidy envelope", () => {
  it("a plain transport error still reads as a sentence", () => {
    expect(visionErrorMessage(new Error("Network request failed"))).toBe(
      "Network request failed.",
    );
  });

  it("an envelope whose error is itself JSON is not shown raw", () => {
    // The nested case from the original report: the backend had wrapped a provider blob.
    const nested = new Error(
      '{"ok":false,"error":"{\\"error\\":\\"runner stopped\\"}","error_code":""}',
    );
    const msg = visionErrorMessage(nested);
    expect(msg).not.toContain("{");
    expect(msg).toBe("That image couldn't be analysed.");
  });

  it("null, undefined and nonsense do not throw", () => {
    for (const bad of [null, undefined, 42, {}, []]) {
      expect(() => visionErrorMessage(bad)).not.toThrow();
    }
  });

  it("never ends in a double full stop", () => {
    const dotted = new Error(
      '{"ok":false,"error":"Something failed.","error_code":"model_runner_stopped"}',
    );
    expect(visionErrorMessage(dotted)).not.toMatch(/\.\./);
  });
});

describe("parsing the envelope out of a transport message", () => {
  it("finds it where it actually sits", () => {
    expect(parseVisionFailure(RUNNER_STOPPED)?.error_code).toBe(
      "model_runner_stopped",
    );
  });

  it("returns null when there is none", () => {
    expect(parseVisionFailure(new Error("Network request failed"))).toBeNull();
    expect(parseVisionFailure(null)).toBeNull();
  });
});