pi-llama-cpp 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -27
- package/package.json +5 -4
- package/src/constants.ts +9 -4
- package/src/enums/action.ts +3 -2
- package/src/enums/mode.ts +1 -0
- package/src/enums/status.ts +1 -0
- package/src/index.ts +33 -28
- package/src/interfaces/auth.ts +1 -5
- package/src/interfaces/endpoints/props.ts +1 -0
- package/src/managers/command.ts +290 -0
- package/src/managers/events.ts +63 -0
- package/src/managers/server.ts +71 -0
- package/src/models/baseModel.ts +68 -20
- package/src/models/legacyModel.ts +45 -0
- package/src/models/routerModel.ts +7 -30
- package/src/models/singleModel.ts +9 -6
- package/src/resolver.ts +123 -0
- package/src/server.ts +171 -0
- package/tests/commandManager.test.ts +182 -133
- package/tests/legacyModel.test.ts +112 -0
- package/tests/mocks.ts +97 -0
- package/tests/resolver.test.ts +163 -104
- package/tests/routerModel.test.ts +46 -68
- package/tests/server.test.ts +175 -0
- package/tests/serverManager.test.ts +117 -0
- package/tests/singleModel.test.ts +21 -29
- package/src/commands/models.ts +0 -228
- package/src/events.ts +0 -26
- package/src/manager.ts +0 -96
- package/src/tools/resolver.ts +0 -136
- package/src/tools/retriever.ts +0 -71
- package/tests/handlers.test.ts +0 -164
- package/tests/modelsCommand.test.ts +0 -270
package/src/manager.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionCommandContext,
|
|
4
|
-
ProviderModelConfig,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { modelsCommand, notFoundCommand } from "./commands/models";
|
|
7
|
-
import {
|
|
8
|
-
DEFAULT_LLAMA_SERVER_URL,
|
|
9
|
-
PROVIDER_ID,
|
|
10
|
-
PROVIDER_NAME,
|
|
11
|
-
} from "./constants";
|
|
12
|
-
import { BaseModel } from "./models/baseModel";
|
|
13
|
-
import { resolveApiKey, resolveUrl } from "./tools/resolver";
|
|
14
|
-
import { isServerReady, listModels } from "./tools/retriever";
|
|
15
|
-
|
|
16
|
-
export class CommandManager {
|
|
17
|
-
private baseUrl: string = DEFAULT_LLAMA_SERVER_URL;
|
|
18
|
-
private serverModels: BaseModel[] = [];
|
|
19
|
-
|
|
20
|
-
constructor(private readonly pi: ExtensionAPI) {}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Sets up the initial state of the provider
|
|
24
|
-
*/
|
|
25
|
-
async initialize() {
|
|
26
|
-
if (await isServerReady()) {
|
|
27
|
-
await this.update();
|
|
28
|
-
} else {
|
|
29
|
-
await this.register([]);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Ensures the models are up-to-date with the server
|
|
35
|
-
*/
|
|
36
|
-
async update() {
|
|
37
|
-
this.baseUrl = `${await resolveUrl(process.cwd())}`;
|
|
38
|
-
|
|
39
|
-
this.serverModels = await listModels();
|
|
40
|
-
const modelConfigs = await Promise.all(
|
|
41
|
-
this.serverModels.map((m) => m.toProviderConfig()),
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
await this.register(modelConfigs);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Registers the provider in Pi with the given configurations
|
|
49
|
-
* Note: Registrations overload previous provider
|
|
50
|
-
*
|
|
51
|
-
* @param models Provider configurations for the models
|
|
52
|
-
*/
|
|
53
|
-
async register(models: ProviderModelConfig[]) {
|
|
54
|
-
this.pi.registerProvider(PROVIDER_ID, {
|
|
55
|
-
name: PROVIDER_NAME,
|
|
56
|
-
baseUrl: this.baseUrl,
|
|
57
|
-
api: "openai-completions",
|
|
58
|
-
apiKey: await resolveApiKey(),
|
|
59
|
-
models,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Dispatches the /models command
|
|
65
|
-
*
|
|
66
|
-
* @param args Arguments passed to the command
|
|
67
|
-
* @param ctx The context used by Pi
|
|
68
|
-
* @param pi The Pi extension
|
|
69
|
-
*/
|
|
70
|
-
async run(args: string, ctx: ExtensionCommandContext) {
|
|
71
|
-
if (!(await isServerReady())) {
|
|
72
|
-
return await notFoundCommand(ctx);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Refresh the model list from the server
|
|
76
|
-
await this.update();
|
|
77
|
-
|
|
78
|
-
// Command: `/models info`
|
|
79
|
-
if (args === "info") {
|
|
80
|
-
const info = await Promise.all(this.serverModels.map((m) => m.getInfo()));
|
|
81
|
-
const message = ctx.ui.theme.fg("accent", info.join("\n"));
|
|
82
|
-
ctx.ui.notify(message, "info");
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Command: `/models unload`
|
|
87
|
-
if (args === "unload") {
|
|
88
|
-
await Promise.all(this.serverModels.map((m) => m.unload()));
|
|
89
|
-
ctx.ui.notify(`Unloaded all ${PROVIDER_NAME} models`, "info");
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Command: `/models` (interactive menu)
|
|
94
|
-
return await modelsCommand(ctx, this.pi, this.serverModels);
|
|
95
|
-
}
|
|
96
|
-
}
|
package/src/tools/resolver.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { access, constants, readFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
API_KEY_PLACEHOLDER,
|
|
6
|
-
DEFAULT_LLAMA_SERVER_URL,
|
|
7
|
-
PROVIDER_ID,
|
|
8
|
-
} from "../constants";
|
|
9
|
-
import { AuthFile } from "../interfaces/auth";
|
|
10
|
-
|
|
11
|
-
// The URL is detected once, to reuse forever
|
|
12
|
-
let resolvedUrl: string | undefined;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Detects if a particular file is present
|
|
16
|
-
* @param filePath The path
|
|
17
|
-
* @returns True if exists
|
|
18
|
-
*/
|
|
19
|
-
const fileExists = async (filePath: string): Promise<boolean> => {
|
|
20
|
-
try {
|
|
21
|
-
await access(filePath, constants.F_OK);
|
|
22
|
-
return true;
|
|
23
|
-
} catch (error) {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Reads and parses the contents of a file as JSON
|
|
30
|
-
* @param filePath The path to the file
|
|
31
|
-
* @returns The parsed content, or null if parsing fails
|
|
32
|
-
*/
|
|
33
|
-
const readContents = async <T>(filePath: string): Promise<T | null> => {
|
|
34
|
-
const raw = await readFile(filePath, "utf-8");
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const contents = JSON.parse(raw);
|
|
38
|
-
return contents;
|
|
39
|
-
} catch (err) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Reads a value from a JSON config file by key
|
|
46
|
-
* @param filePath Path to the JSON config file
|
|
47
|
-
* @param key Key to extract from the parsed JSON
|
|
48
|
-
* @returns The value at the given key, or null if file/key missing or invalid
|
|
49
|
-
*/
|
|
50
|
-
const readConfigValue = async <T>(
|
|
51
|
-
filePath: string,
|
|
52
|
-
key: keyof T,
|
|
53
|
-
): Promise<T[keyof T] | null> => {
|
|
54
|
-
const cfg = await readContents<T>(filePath);
|
|
55
|
-
return cfg?.[key] ?? null;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Reads API key from Pi's auth file
|
|
60
|
-
* @returns The API key, as defined by the auth.json file
|
|
61
|
-
*/
|
|
62
|
-
export const resolveApiKey = async (): Promise<string> => {
|
|
63
|
-
const authPath = join(getAgentDir(), "settings.json");
|
|
64
|
-
if (!(await fileExists(authPath))) return API_KEY_PLACEHOLDER;
|
|
65
|
-
|
|
66
|
-
const cfg = await readConfigValue<AuthFile>(authPath, PROVIDER_ID);
|
|
67
|
-
return cfg?.key ?? API_KEY_PLACEHOLDER;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Resolves the llama-server url by searching for it in the global settings.json file
|
|
72
|
-
* @returns The URL, if found.
|
|
73
|
-
*/
|
|
74
|
-
const resolveGlobalUrl = async (): Promise<string | null> => {
|
|
75
|
-
const globalPath = join(getAgentDir(), "settings.json");
|
|
76
|
-
if (!(await fileExists(globalPath))) return null;
|
|
77
|
-
|
|
78
|
-
return readConfigValue<Record<string, string>>(globalPath, "llamaServerUrl");
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Resolves the llama-server url by searching for it in the project's .pi/llama-server.json file
|
|
83
|
-
* @param cwd The current working directory
|
|
84
|
-
* @returns The URL, if found.
|
|
85
|
-
*/
|
|
86
|
-
const resolveProjectUrl = async (cwd: string): Promise<string | null> => {
|
|
87
|
-
const projectPath = join(cwd, ".pi", "llama-server.json");
|
|
88
|
-
|
|
89
|
-
if (!(await fileExists(projectPath))) return null;
|
|
90
|
-
return readConfigValue<Record<string, string>>(projectPath, "url");
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Resolves the llama-server url by searching for it in the environment
|
|
95
|
-
* @returns The URL, if found.
|
|
96
|
-
*/
|
|
97
|
-
const resolveEnvUrl = async (): Promise<string | null> => {
|
|
98
|
-
return process.env.LLAMA_SERVER_URL ?? null;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Tries all possible ways to retrieve the llama-server URL
|
|
103
|
-
* @param cwd The current working directory
|
|
104
|
-
* @returns The URL, or a default if not found
|
|
105
|
-
*/
|
|
106
|
-
const resolveUrlWithFallbacks = async (cwd: string): Promise<string> => {
|
|
107
|
-
// 1. per-project config
|
|
108
|
-
let response = await resolveProjectUrl(cwd);
|
|
109
|
-
if (response) return response;
|
|
110
|
-
|
|
111
|
-
// 2. env
|
|
112
|
-
response = await resolveEnvUrl();
|
|
113
|
-
if (response) return response;
|
|
114
|
-
|
|
115
|
-
// 3. global settings: ~/.pi/agent/settings.json
|
|
116
|
-
response = await resolveGlobalUrl();
|
|
117
|
-
if (response) return response;
|
|
118
|
-
|
|
119
|
-
// 4. default
|
|
120
|
-
return DEFAULT_LLAMA_SERVER_URL;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Resolves the URL where llama-server is running
|
|
125
|
-
* @param cwd The current working directory
|
|
126
|
-
* @returns The URL, or a default if not found
|
|
127
|
-
*/
|
|
128
|
-
export const resolveUrl = async (cwd: string): Promise<string> => {
|
|
129
|
-
if (resolvedUrl) return resolvedUrl;
|
|
130
|
-
const result = await resolveUrlWithFallbacks(cwd);
|
|
131
|
-
|
|
132
|
-
// Strip trailing slashes
|
|
133
|
-
resolvedUrl = result.replace(/\/+$/, "");
|
|
134
|
-
|
|
135
|
-
return resolvedUrl;
|
|
136
|
-
};
|
package/src/tools/retriever.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { HealthEndpoint } from "../interfaces/endpoints/health";
|
|
2
|
-
import { ModelsEndpoint } from "../interfaces/endpoints/models";
|
|
3
|
-
import { BaseModel } from "../models/baseModel";
|
|
4
|
-
import { RouterModel } from "../models/routerModel";
|
|
5
|
-
import { SingleModel } from "../models/singleModel";
|
|
6
|
-
import { resolveApiKey, resolveUrl } from "./resolver";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Detects if the server is ready
|
|
10
|
-
* @returns True if it's ready to work
|
|
11
|
-
*/
|
|
12
|
-
export const isServerReady = async (): Promise<boolean> => {
|
|
13
|
-
try {
|
|
14
|
-
const { status } = await rpc<HealthEndpoint>("/health");
|
|
15
|
-
return status === "ok";
|
|
16
|
-
} catch {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Makes an HTTP request to the llama-server and returns the parsed JSON response
|
|
23
|
-
*
|
|
24
|
-
* @param endpoint The endpoint path to fetch (e.g. "/health")
|
|
25
|
-
* @param body The optional request body for POST requests
|
|
26
|
-
* @returns The parsed JSON response from the server
|
|
27
|
-
*/
|
|
28
|
-
export const rpc = async <T>(
|
|
29
|
-
endpoint: string,
|
|
30
|
-
body?: Record<string, unknown>,
|
|
31
|
-
): Promise<T> => {
|
|
32
|
-
const base = await resolveUrl(process.cwd());
|
|
33
|
-
const url = `${base}${endpoint}`;
|
|
34
|
-
|
|
35
|
-
const data = {
|
|
36
|
-
method: body ? "POST" : "GET",
|
|
37
|
-
headers: body ? { "Content-Type": "application/json" } : undefined,
|
|
38
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const apiKey = await resolveApiKey();
|
|
42
|
-
const res = await fetch(url, {
|
|
43
|
-
...data,
|
|
44
|
-
headers: {
|
|
45
|
-
...data.headers,
|
|
46
|
-
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const response: T = await res.json();
|
|
51
|
-
return response;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Retrieves a list of available models from llama-server
|
|
56
|
-
* @param base Base URL to use
|
|
57
|
-
* @returns The list of models
|
|
58
|
-
*/
|
|
59
|
-
export const listModels = async (): Promise<BaseModel[]> => {
|
|
60
|
-
const { models, data } = await rpc<ModelsEndpoint>("/models");
|
|
61
|
-
|
|
62
|
-
if (models) {
|
|
63
|
-
return data.map((m) => new SingleModel(m));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const response = data
|
|
67
|
-
.map((m) => new RouterModel(m))
|
|
68
|
-
.sort((a, b) => (a.id > b.id ? 1 : a.id === b.id ? 0 : -1));
|
|
69
|
-
|
|
70
|
-
return response;
|
|
71
|
-
};
|
package/tests/handlers.test.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { Action } from "../src/enums/action";
|
|
3
|
-
import { Mode } from "../src/enums/mode";
|
|
4
|
-
import { Status } from "../src/enums/status";
|
|
5
|
-
import { DataProperty } from "../src/interfaces/endpoints/models";
|
|
6
|
-
|
|
7
|
-
// Mock the retriever module before importing anything that depends on it
|
|
8
|
-
vi.mock("../src/tools/retriever", () => ({
|
|
9
|
-
rpc: vi.fn(),
|
|
10
|
-
isServerReady: vi.fn(),
|
|
11
|
-
listModels: vi.fn(),
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
class TestModel {
|
|
15
|
-
constructor(
|
|
16
|
-
private readonly model: DataProperty,
|
|
17
|
-
private readonly _mode: Mode,
|
|
18
|
-
private readonly _status: Status,
|
|
19
|
-
) {}
|
|
20
|
-
|
|
21
|
-
get mode(): Mode {
|
|
22
|
-
return this._mode;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
get capabilities(): ["text"] | ["image"] {
|
|
26
|
-
return ["text"];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async getStatus(): Promise<Status> {
|
|
30
|
-
return this._status;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async getContextSize(): Promise<number> {
|
|
34
|
-
return 4096;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const createModel = (
|
|
39
|
-
mode: Mode,
|
|
40
|
-
status: Status,
|
|
41
|
-
overrides: Partial<DataProperty> = {},
|
|
42
|
-
) =>
|
|
43
|
-
new TestModel(
|
|
44
|
-
{
|
|
45
|
-
id: "test",
|
|
46
|
-
tags: [],
|
|
47
|
-
object: "model",
|
|
48
|
-
owned_by: "test",
|
|
49
|
-
created: Date.now(),
|
|
50
|
-
...overrides,
|
|
51
|
-
},
|
|
52
|
-
mode,
|
|
53
|
-
status,
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Replicates the getActionsForModel logic from handlers.ts for testing
|
|
58
|
-
* without needing the full Pi extension context.
|
|
59
|
-
*/
|
|
60
|
-
const getActionsForModel = async (model: TestModel): Promise<Array<Action>> => {
|
|
61
|
-
const routerModeActions: Record<Status, Array<Action>> = {
|
|
62
|
-
[Status.LOADED]: [Action.SWITCH, Action.UNLOAD, Action.INFO, Action.CANCEL],
|
|
63
|
-
[Status.LOADING]: [Action.INFO, Action.CANCEL],
|
|
64
|
-
[Status.FAILED]: [Action.RETRY, Action.CANCEL],
|
|
65
|
-
[Status.SLEEPING]: [
|
|
66
|
-
Action.SWITCH,
|
|
67
|
-
Action.UNLOAD,
|
|
68
|
-
Action.INFO,
|
|
69
|
-
Action.CANCEL,
|
|
70
|
-
],
|
|
71
|
-
[Status.UNLOADED]: [Action.LOAD, Action.CANCEL],
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const singleModeActions: Record<Status, Array<Action>> = {
|
|
75
|
-
[Status.LOADED]: [Action.INFO, Action.CANCEL],
|
|
76
|
-
[Status.LOADING]: [Action.CANCEL],
|
|
77
|
-
[Status.FAILED]: [Action.CANCEL],
|
|
78
|
-
[Status.SLEEPING]: [Action.INFO, Action.CANCEL],
|
|
79
|
-
[Status.UNLOADED]: [Action.CANCEL],
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const allActions =
|
|
83
|
-
model.mode === Mode.ROUTER ? routerModeActions : singleModeActions;
|
|
84
|
-
|
|
85
|
-
const status = await model.getStatus();
|
|
86
|
-
return allActions[status];
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
describe("Action availability", () => {
|
|
90
|
-
const actionMatrix: Array<{
|
|
91
|
-
mode: Mode;
|
|
92
|
-
status: Status;
|
|
93
|
-
expected: Action[];
|
|
94
|
-
}> = [
|
|
95
|
-
// Router mode
|
|
96
|
-
{
|
|
97
|
-
mode: Mode.ROUTER,
|
|
98
|
-
status: Status.LOADED,
|
|
99
|
-
expected: [Action.SWITCH, Action.UNLOAD, Action.INFO, Action.CANCEL],
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
mode: Mode.ROUTER,
|
|
103
|
-
status: Status.LOADING,
|
|
104
|
-
expected: [Action.INFO, Action.CANCEL],
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
mode: Mode.ROUTER,
|
|
108
|
-
status: Status.FAILED,
|
|
109
|
-
expected: [Action.RETRY, Action.CANCEL],
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
mode: Mode.ROUTER,
|
|
113
|
-
status: Status.SLEEPING,
|
|
114
|
-
expected: [Action.SWITCH, Action.UNLOAD, Action.INFO, Action.CANCEL],
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
mode: Mode.ROUTER,
|
|
118
|
-
status: Status.UNLOADED,
|
|
119
|
-
expected: [Action.LOAD, Action.CANCEL],
|
|
120
|
-
},
|
|
121
|
-
// Single mode
|
|
122
|
-
{
|
|
123
|
-
mode: Mode.SINGLE,
|
|
124
|
-
status: Status.LOADED,
|
|
125
|
-
expected: [Action.INFO, Action.CANCEL],
|
|
126
|
-
},
|
|
127
|
-
{ mode: Mode.SINGLE, status: Status.LOADING, expected: [Action.CANCEL] },
|
|
128
|
-
{ mode: Mode.SINGLE, status: Status.FAILED, expected: [Action.CANCEL] },
|
|
129
|
-
{
|
|
130
|
-
mode: Mode.SINGLE,
|
|
131
|
-
status: Status.SLEEPING,
|
|
132
|
-
expected: [Action.INFO, Action.CANCEL],
|
|
133
|
-
},
|
|
134
|
-
{ mode: Mode.SINGLE, status: Status.UNLOADED, expected: [Action.CANCEL] },
|
|
135
|
-
];
|
|
136
|
-
|
|
137
|
-
it.each(actionMatrix)(
|
|
138
|
-
"should return correct actions for $mode/$status",
|
|
139
|
-
async ({ mode, status, expected }) => {
|
|
140
|
-
const model = createModel(mode, status);
|
|
141
|
-
const actions = await getActionsForModel(model);
|
|
142
|
-
expect(actions).toEqual(expected);
|
|
143
|
-
},
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
it("should always include CANCEL regardless of mode or status", async () => {
|
|
147
|
-
for (const mode of [Mode.ROUTER, Mode.SINGLE]) {
|
|
148
|
-
for (const status of Object.values(Status)) {
|
|
149
|
-
const model = createModel(mode, status);
|
|
150
|
-
const actions = await getActionsForModel(model);
|
|
151
|
-
expect(actions).toContain(Action.CANCEL);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("should not include mode-exclusive actions", async () => {
|
|
157
|
-
const singleLoaded = createModel(Mode.SINGLE, Status.LOADED);
|
|
158
|
-
expect(await getActionsForModel(singleLoaded)).not.toContain(Action.SWITCH);
|
|
159
|
-
expect(await getActionsForModel(singleLoaded)).not.toContain(Action.LOAD);
|
|
160
|
-
|
|
161
|
-
const singleFailed = createModel(Mode.SINGLE, Status.FAILED);
|
|
162
|
-
expect(await getActionsForModel(singleFailed)).not.toContain(Action.RETRY);
|
|
163
|
-
});
|
|
164
|
-
});
|