File size: 4,745 Bytes
5c2a829
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import { beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest";
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";

const mocks = vi.hoisted(() => ({
  getSnapshot: vi.fn(),
  loadCatalog: vi.fn(),
}));

vi.mock("../agents/prepared-model-catalog.js", () => ({
  loadProviderScopedThinkingCatalog: vi.fn(async () => []),
  getPreparedModelCatalogSnapshot: (...args: unknown[]) => mocks.getSnapshot(...args),
  readPreparedModelCatalog: (...args: unknown[]) => mocks.loadCatalog(...args),
}));

import {
  loadModelCatalog,
  loadPreparedModelCatalog,
  resolveThinkingDefaultWithRuntimeCatalog,
} from "openclaw/plugin-sdk/agent-runtime";

describe("agent-runtime model catalog compatibility", () => {
  beforeEach(() => {
    mocks.getSnapshot.mockReset();
    mocks.loadCatalog.mockReset();
  });

  it("uses the shipped thinking catalog callback", async () => {
    const readCatalog = vi.fn(async () => []);

    await expect(
      resolveThinkingDefaultWithRuntimeCatalog({
        cfg: { agents: { defaults: { thinkingDefault: "low" } } },
        provider: "example",
        model: "example-model",
        loadModelCatalog: readCatalog,
      }),
    ).resolves.toBe("low");
    expect(readCatalog).toHaveBeenCalledOnce();
  });

  it("propagates failures from the shipped thinking catalog callback", async () => {
    const failure = new Error("catalog unavailable");

    await expect(
      resolveThinkingDefaultWithRuntimeCatalog({
        cfg: {},
        provider: "example",
        model: "example-model",
        loadModelCatalog: async () => {
          throw failure;
        },
      }),
    ).rejects.toBe(failure);
  });

  it.each([
    ["prepared", loadPreparedModelCatalog],
    ["legacy", loadModelCatalog],
  ] as const)("preserves the writable default of the %s SDK loader", async (_name, load) => {
    const entries = [{ provider: "test", id: "discovered", name: "Discovered" }];
    mocks.loadCatalog.mockResolvedValue(entries);

    await expect(load()).resolves.toBe(entries);
    expect(mocks.loadCatalog).toHaveBeenCalledExactlyOnceWith({ readOnly: false });
    expect(mocks.getSnapshot).not.toHaveBeenCalled();
  });

  it.each([true, false])("preserves explicit readOnly:%s in the SDK loader", async (readOnly) => {
    const config = {};
    const entries = [{ provider: "test", id: "selected", name: "Selected" }];
    mocks.loadCatalog.mockResolvedValue(entries);

    await expect(loadPreparedModelCatalog({ config, readOnly })).resolves.toBe(entries);
    expect(mocks.loadCatalog).toHaveBeenCalledExactlyOnceWith({ config, readOnly });
  });

  it("keeps legacy cache-only reads nonblocking", async () => {
    mocks.getSnapshot.mockReturnValue({
      entries: [{ provider: "test", id: "cached", name: "Cached" }],
      routeVariants: [],
    });

    await expect(
      loadModelCatalog({ cacheOnly: true, useCache: true, refreshFullCatalog: true }),
    ).resolves.toEqual([{ provider: "test", id: "cached", name: "Cached" }]);
    expect(mocks.loadCatalog).not.toHaveBeenCalled();
  });

  it("preserves explicit refresh intent through the legacy loader", async () => {
    const entries = [{ provider: "test", id: "refreshed", name: "Refreshed" }];
    mocks.loadCatalog.mockResolvedValue(entries);

    await expect(loadModelCatalog({ refreshFullCatalog: true })).resolves.toBe(entries);
    expect(mocks.loadCatalog).toHaveBeenCalledExactlyOnceWith({
      readOnly: false,
      refreshFullCatalog: true,
    });
    expect(mocks.getSnapshot).not.toHaveBeenCalled();
  });

  it("accepts legacy options without overriding lifecycle metadata", async () => {
    type LegacyMetadataSnapshot = Omit<
      PluginMetadataSnapshot,
      "owners" | "declaredProviderOwners"
    > & {
      owners: Omit<PluginMetadataSnapshot["owners"], "modelIdNormalizationPolicies">;
    };
    type AcceptedMetadataSnapshot = NonNullable<
      NonNullable<Parameters<typeof loadModelCatalog>[0]>["metadataSnapshot"]
    >;
    expectTypeOf<LegacyMetadataSnapshot>().toMatchTypeOf<AcceptedMetadataSnapshot>();
    expectTypeOf<PluginMetadataSnapshot>().toMatchTypeOf<AcceptedMetadataSnapshot>();
    mocks.loadCatalog.mockResolvedValue([]);
    const config = {};
    const env = { OPENCLAW_STATE_DIR: "/tmp/plugin-state" };

    await loadModelCatalog({
      agentDir: "/tmp/plugin-agent",
      config,
      env,
      metadataSnapshot: {} as never,
      readOnly: true,
      useCache: false,
      workspaceDir: "/tmp/plugin-workspace",
    });

    expect(mocks.loadCatalog).toHaveBeenCalledWith({
      agentDir: "/tmp/plugin-agent",
      config,
      env,
      readOnly: true,
      workspaceDir: "/tmp/plugin-workspace",
    });
  });
});