arisa 5.1.60 → 5.1.64
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 +9 -0
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +10 -0
- package/src/core/config/config-defaults.js +28 -1
- package/src/core/tasks/task-runner.js +17 -2
- package/src/core/tasks/task-store.js +80 -14
- package/src/core/tools/tool-registry.js +20 -5
- package/src/core/tools/weighted-resource-governor.js +153 -0
- package/src/index.js +20 -0
- package/src/official-tools.lock.json +56 -42
- package/src/runtime/create-app.js +1 -1
- package/src/runtime/create-headless-app.js +2 -2
- package/src/runtime/paths.js +4 -0
- package/src/runtime/service-manager.js +3 -1
- package/src/runtime/service-supervisor.js +98 -0
- package/src/transport/telegram/bot.js +77 -9
- package/src/transport/telegram/chat-queue.js +13 -2
- package/src/transport/telegram/reply-topic-routing.js +111 -0
- package/src/transport/telegram/task-dispatcher.js +26 -3
- package/src/transport/telegram/telegram-session-bridge.js +7 -0
- package/src/transport/telegram/workspace-topic-store.js +228 -0
- package/test/chat-queue.test.js +32 -0
- package/test/model-selection.test.js +9 -1
- package/test/official-tool-dependencies.test.js +1 -1
- package/test/paths.test.js +8 -0
- package/test/service-manager.test.js +48 -0
- package/test/task-store.test.js +56 -4
- package/test/telegram-reply-topic-routing.test.js +94 -0
- package/test/telegram-task-dispatcher.test.js +51 -0
- package/test/telegram-workspace-topic-store.test.js +124 -0
- package/test/tool-registry-run.test.js +41 -0
- package/test/weighted-resource-governor.test.js +95 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
appendGeneralReplyRoutingInstruction,
|
|
5
|
+
extractReplyTopicMetadata,
|
|
6
|
+
isGeneralWorkspaceRoute,
|
|
7
|
+
routeGeneralWorkspaceReply
|
|
8
|
+
} from "../src/transport/telegram/reply-topic-routing.js";
|
|
9
|
+
|
|
10
|
+
const topics = [
|
|
11
|
+
{ threadId: 23, name: "Stories", description: "Published stories and editorial work" },
|
|
12
|
+
{ threadId: 87, name: "CBPR", description: "Castle Bravo press campaign" },
|
|
13
|
+
{ threadId: 114, name: "CORE", description: "Arisa core engineering" }
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const generalRoute = {
|
|
17
|
+
workspace: true,
|
|
18
|
+
ownerChatId: 42,
|
|
19
|
+
sessionId: "42",
|
|
20
|
+
scopeChatId: 42,
|
|
21
|
+
transportChatId: -100123,
|
|
22
|
+
threadId: null,
|
|
23
|
+
topicThreadId: 1,
|
|
24
|
+
generalTopicId: 1
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
test("General prompts describe conservative dynamic reply routing", () => {
|
|
28
|
+
const prompt = appendGeneralReplyRoutingInstruction("Incoming Telegram message.", topics, [
|
|
29
|
+
{ name: "Infrastructure", proposedAt: "2026-08-24T00:00:00.000Z" }
|
|
30
|
+
]);
|
|
31
|
+
assert.match(prompt, /only because the incoming message was written in General/);
|
|
32
|
+
assert.match(prompt, /never applies to a private chat/);
|
|
33
|
+
assert.match(prompt, /original message must stay in General/);
|
|
34
|
+
assert.match(prompt, /23: Stories/);
|
|
35
|
+
assert.match(prompt, /87: CBPR/);
|
|
36
|
+
assert.match(prompt, /114: CORE/);
|
|
37
|
+
assert.match(prompt, /Do not repeat these recent topic proposals: Infrastructure/);
|
|
38
|
+
assert.match(prompt, /Never create a topic without explicit confirmation/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("an allowed trailing marker is stripped and selects its topic", () => {
|
|
42
|
+
assert.deepEqual(extractReplyTopicMetadata("Implemented.\n[[ARISA_REPLY_TOPIC:114]]", topics), {
|
|
43
|
+
text: "Implemented.",
|
|
44
|
+
threadId: 114,
|
|
45
|
+
proposal: ""
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("invalid markers are stripped without rerouting", () => {
|
|
50
|
+
assert.deepEqual(extractReplyTopicMetadata("Keep this here.\n[[ARISA_REPLY_TOPIC:999]]", topics), {
|
|
51
|
+
text: "Keep this here.",
|
|
52
|
+
threadId: null,
|
|
53
|
+
proposal: ""
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("topic proposals are stripped and returned as transport metadata", () => {
|
|
58
|
+
assert.deepEqual(extractReplyTopicMetadata("Should we create it?\n[[ARISA_PROPOSE_TOPIC:Research Lab]]", topics), {
|
|
59
|
+
text: "Should we create it?",
|
|
60
|
+
threadId: null,
|
|
61
|
+
proposal: "Research Lab"
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("only replies originating in a supergroup General topic can change destination", () => {
|
|
66
|
+
assert.equal(isGeneralWorkspaceRoute(generalRoute), true);
|
|
67
|
+
const routed = routeGeneralWorkspaceReply({
|
|
68
|
+
route: generalRoute,
|
|
69
|
+
topics,
|
|
70
|
+
text: "Core update.\n[[ARISA_REPLY_TOPIC:114]]"
|
|
71
|
+
});
|
|
72
|
+
assert.equal(routed.text, "Core update.");
|
|
73
|
+
assert.equal(routed.route.sessionId, "42");
|
|
74
|
+
assert.equal(routed.route.threadId, 114);
|
|
75
|
+
assert.equal(routed.topic.name, "CORE");
|
|
76
|
+
|
|
77
|
+
const privateRoute = {
|
|
78
|
+
workspace: false,
|
|
79
|
+
sessionId: "42",
|
|
80
|
+
scopeChatId: 42,
|
|
81
|
+
transportChatId: 42,
|
|
82
|
+
threadId: null
|
|
83
|
+
};
|
|
84
|
+
assert.equal(isGeneralWorkspaceRoute(privateRoute), false);
|
|
85
|
+
const privateReply = routeGeneralWorkspaceReply({
|
|
86
|
+
route: privateRoute,
|
|
87
|
+
topics,
|
|
88
|
+
text: "Private response.\n[[ARISA_REPLY_TOPIC:114]]"
|
|
89
|
+
});
|
|
90
|
+
assert.equal(privateReply.route, privateRoute);
|
|
91
|
+
assert.equal(privateReply.topic, null);
|
|
92
|
+
assert.equal(privateReply.proposal, "");
|
|
93
|
+
assert.equal(privateReply.text, "Private response.");
|
|
94
|
+
});
|
|
@@ -50,12 +50,33 @@ test("confirms an agent task only after prompt execution resolves", async () =>
|
|
|
50
50
|
await new Promise((resolve) => setImmediate(resolve));
|
|
51
51
|
assert.equal(calls[0][0], "enqueue");
|
|
52
52
|
assert.deepEqual(calls[0][1].route, { transport: "telegram", destination: { chatId: -1001, threadId: 9 } });
|
|
53
|
+
assert.equal(calls[0][1].timeoutMs, 15 * 60_000);
|
|
53
54
|
assert.equal(calls.some(([name]) => name === "complete"), false);
|
|
54
55
|
confirmExecution();
|
|
55
56
|
await running;
|
|
56
57
|
assert.deepEqual(calls.at(-1), ["complete", "task-1"]);
|
|
57
58
|
});
|
|
58
59
|
|
|
60
|
+
test("passes bounded execution deadlines to scheduled prompts", async () => {
|
|
61
|
+
const { calls, dispatcher } = createHarness({
|
|
62
|
+
dependencies: { taskTimeouts: { agentTimeoutMs: 900, eventTimeoutMs: 300 } }
|
|
63
|
+
});
|
|
64
|
+
await dispatcher.runClaimedTask({
|
|
65
|
+
id: "deadline-task",
|
|
66
|
+
kind: "agent_task",
|
|
67
|
+
payload: { chatId: 123, prompt: "work" }
|
|
68
|
+
});
|
|
69
|
+
await dispatcher.runClaimedTask({
|
|
70
|
+
id: "deadline-event",
|
|
71
|
+
kind: "agent_event",
|
|
72
|
+
payload: { chatId: 123, prompt: "event" }
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const enqueues = calls.filter(([name]) => name === "enqueue");
|
|
76
|
+
assert.equal(enqueues[0][1].timeoutMs, 900);
|
|
77
|
+
assert.equal(enqueues[1][1].timeoutMs, 300);
|
|
78
|
+
});
|
|
79
|
+
|
|
59
80
|
test("acknowledges an agent event before executing it", async () => {
|
|
60
81
|
const { calls, dispatcher } = createHarness();
|
|
61
82
|
await dispatcher.runClaimedTask({
|
|
@@ -155,6 +176,36 @@ test("notifies the routed Telegram topic once retries are exhausted", async () =
|
|
|
155
176
|
assert.equal(calls.at(-1)[2].includes("private payload"), false);
|
|
156
177
|
});
|
|
157
178
|
|
|
179
|
+
test("serializes tasks within one destination while independent destinations continue", async () => {
|
|
180
|
+
const tasks = [
|
|
181
|
+
{ id: "first", kind: "agent_task", payload: { chatId: 123, prompt: "first" }, route: { transport: "telegram", destination: { chatId: -1001, threadId: 87 } } },
|
|
182
|
+
{ id: "second", kind: "agent_task", payload: { chatId: 123, prompt: "second" }, route: { transport: "telegram", destination: { chatId: -1001, threadId: 87 } } },
|
|
183
|
+
{ id: "other", kind: "agent_task", payload: { chatId: 123, prompt: "other" }, route: { transport: "telegram", destination: { chatId: -1001, threadId: 23 } } }
|
|
184
|
+
];
|
|
185
|
+
let releaseFirst;
|
|
186
|
+
const firstExecution = new Promise((resolve) => { releaseFirst = resolve; });
|
|
187
|
+
const { calls, dispatcher } = createHarness({
|
|
188
|
+
taskStore: { async claimDue() { return tasks; } },
|
|
189
|
+
dependencies: {
|
|
190
|
+
enqueueAsyncPrompt: async (input) => {
|
|
191
|
+
calls.push(["enqueue", input.label]);
|
|
192
|
+
if (input.label.includes("first")) await firstExecution;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const dispatching = dispatcher.dispatchDueTasks();
|
|
198
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
199
|
+
assert.ok(calls.some((call) => call[0] === "enqueue" && call[1].includes("first")));
|
|
200
|
+
assert.ok(calls.some((call) => call[0] === "enqueue" && call[1].includes("other")));
|
|
201
|
+
assert.equal(calls.some((call) => call[0] === "enqueue" && call[1].includes("second")), false);
|
|
202
|
+
|
|
203
|
+
releaseFirst();
|
|
204
|
+
await dispatching;
|
|
205
|
+
const enqueueOrder = calls.filter((call) => call[0] === "enqueue").map((call) => call[1]);
|
|
206
|
+
assert.ok(enqueueOrder.indexOf("scheduled task first") < enqueueOrder.indexOf("scheduled task second"));
|
|
207
|
+
});
|
|
208
|
+
|
|
158
209
|
test("due-task dispatch retries one failure without blocking another task", async () => {
|
|
159
210
|
const tasks = [
|
|
160
211
|
{ id: "bad", kind: "agent_task", payload: { chatId: 123, prompt: "fail" } },
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
6
|
+
import {
|
|
7
|
+
migrateLegacyReplyTopics,
|
|
8
|
+
WorkspaceTopicStore
|
|
9
|
+
} from "../src/transport/telegram/workspace-topic-store.js";
|
|
10
|
+
|
|
11
|
+
async function fixture(now = Date.parse("2026-08-24T12:00:00.000Z")) {
|
|
12
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "arisa-workspace-topics-"));
|
|
13
|
+
const file = path.join(directory, "topics.json");
|
|
14
|
+
return {
|
|
15
|
+
file,
|
|
16
|
+
store: new WorkspaceTopicStore({ resolveFile: () => file, now: () => now }),
|
|
17
|
+
cleanup: () => rm(directory, { recursive: true, force: true })
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("workspace topics are learned, renamed, closed, and reopened dynamically", async () => {
|
|
22
|
+
const { store, cleanup } = await fixture();
|
|
23
|
+
try {
|
|
24
|
+
const route = {
|
|
25
|
+
workspace: true,
|
|
26
|
+
ownerChatId: 42,
|
|
27
|
+
transportChatId: -100123,
|
|
28
|
+
topicThreadId: 25,
|
|
29
|
+
generalTopicId: 1
|
|
30
|
+
};
|
|
31
|
+
await store.observeMessage(route, { forum_topic_created: { name: "Research" } });
|
|
32
|
+
assert.equal((await store.listTopics(42, -100123))[0].name, "Research");
|
|
33
|
+
|
|
34
|
+
await store.observeMessage(route, { forum_topic_edited: { name: "Research Lab" } });
|
|
35
|
+
assert.equal((await store.listTopics(42, -100123))[0].name, "Research Lab");
|
|
36
|
+
|
|
37
|
+
await store.observeMessage(route, { forum_topic_closed: {} });
|
|
38
|
+
assert.deepEqual(await store.listTopics(42, -100123), []);
|
|
39
|
+
assert.equal((await store.listTopics(42, -100123, { includeClosed: true }))[0].status, "closed");
|
|
40
|
+
|
|
41
|
+
await store.observeMessage(route, { forum_topic_reopened: {} });
|
|
42
|
+
assert.equal((await store.listTopics(42, -100123))[0].status, "open");
|
|
43
|
+
} finally {
|
|
44
|
+
await cleanup();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("Arisa topic initialization persists semantic context", async () => {
|
|
49
|
+
const { store, cleanup } = await fixture();
|
|
50
|
+
try {
|
|
51
|
+
await store.upsertTopic(42, -100123, {
|
|
52
|
+
threadId: 114,
|
|
53
|
+
name: "CORE",
|
|
54
|
+
description: "Arisa runtime and Telegram engineering",
|
|
55
|
+
source: "arisa-initialized"
|
|
56
|
+
});
|
|
57
|
+
assert.deepEqual((await store.listTopics(42, -100123)).map(({ threadId, name, description, source }) => ({
|
|
58
|
+
threadId,
|
|
59
|
+
name,
|
|
60
|
+
description,
|
|
61
|
+
source
|
|
62
|
+
})), [{
|
|
63
|
+
threadId: 114,
|
|
64
|
+
name: "CORE",
|
|
65
|
+
description: "Arisa runtime and Telegram engineering",
|
|
66
|
+
source: "arisa-initialized"
|
|
67
|
+
}]);
|
|
68
|
+
} finally {
|
|
69
|
+
await cleanup();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("concurrent topic updates preserve every workspace entry", async () => {
|
|
74
|
+
const { store, file, cleanup } = await fixture();
|
|
75
|
+
try {
|
|
76
|
+
await Promise.all(Array.from({ length: 30 }, (_, index) => store.upsertTopic(42, -100123, {
|
|
77
|
+
threadId: index + 2,
|
|
78
|
+
name: `Topic ${index + 2}`
|
|
79
|
+
})));
|
|
80
|
+
assert.equal((await store.listTopics(42, -100123)).length, 30);
|
|
81
|
+
assert.equal(JSON.parse(await readFile(file, "utf8")).version, 1);
|
|
82
|
+
} finally {
|
|
83
|
+
await cleanup();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("legacy configured topics migrate once into dynamic state", async () => {
|
|
88
|
+
const { store, cleanup } = await fixture();
|
|
89
|
+
try {
|
|
90
|
+
const config = {
|
|
91
|
+
telegram: {
|
|
92
|
+
ownerWorkspaceGroups: {
|
|
93
|
+
"-100123": {
|
|
94
|
+
ownerChatId: 42,
|
|
95
|
+
generalTopicId: 1,
|
|
96
|
+
replyTopics: {
|
|
97
|
+
"1": { name: "General" },
|
|
98
|
+
"23": { name: "Stories", description: "Editorial work" }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
assert.equal(await migrateLegacyReplyTopics(config, store), 1);
|
|
105
|
+
assert.equal(config.telegram.ownerWorkspaceGroups["-100123"].replyTopics, undefined);
|
|
106
|
+
assert.deepEqual((await store.listTopics(42, -100123)).map((topic) => topic.name), ["Stories"]);
|
|
107
|
+
assert.equal(await migrateLegacyReplyTopics(config, store), 0);
|
|
108
|
+
} finally {
|
|
109
|
+
await cleanup();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("recent proposals are deduplicated and scoped by supergroup", async () => {
|
|
114
|
+
const { store, cleanup } = await fixture();
|
|
115
|
+
try {
|
|
116
|
+
await store.recordProposal(42, -100123, "Research Lab");
|
|
117
|
+
await store.recordProposal(42, -100123, "research lab");
|
|
118
|
+
await store.recordProposal(42, -100999, "Another Group");
|
|
119
|
+
assert.deepEqual((await store.listRecentProposals(42, -100123)).map((item) => item.name), ["research lab"]);
|
|
120
|
+
assert.deepEqual((await store.listRecentProposals(42, -100999)).map((item) => item.name), ["Another Group"]);
|
|
121
|
+
} finally {
|
|
122
|
+
await cleanup();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
@@ -157,6 +157,22 @@ test("lists optional semantic metadata with stable defaults", async () => {
|
|
|
157
157
|
|
|
158
158
|
assert.equal(registry.list()[0].category, null);
|
|
159
159
|
assert.deepEqual(registry.list()[0].keywords, []);
|
|
160
|
+
assert.equal(registry.get("fake-tool").execution, null);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("loads weighted execution metadata from the tool manifest", async () => {
|
|
164
|
+
await resetHome();
|
|
165
|
+
await createFakeTool("heavy-tool", {
|
|
166
|
+
execution: { resourceClass: "browser", weight: 2 }
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const registry = new ToolRegistry();
|
|
170
|
+
await registry.load();
|
|
171
|
+
|
|
172
|
+
assert.deepEqual(registry.get("heavy-tool").execution, {
|
|
173
|
+
resourceClass: "browser",
|
|
174
|
+
weight: 2
|
|
175
|
+
});
|
|
160
176
|
});
|
|
161
177
|
|
|
162
178
|
test("shows semantic metadata in tool help", async () => {
|
|
@@ -193,6 +209,31 @@ test("reports dependency status in help and blocks a tool with a missing depende
|
|
|
193
209
|
);
|
|
194
210
|
});
|
|
195
211
|
|
|
212
|
+
test("wraps declared tool runs in the shared execution governor", async () => {
|
|
213
|
+
await resetHome();
|
|
214
|
+
await createFakeTool("heavy-tool", {
|
|
215
|
+
execution: { resourceClass: "browser", weight: 1 }
|
|
216
|
+
});
|
|
217
|
+
const calls = [];
|
|
218
|
+
const executionGovernor = {
|
|
219
|
+
acquire: async (execution, label) => {
|
|
220
|
+
calls.push({ type: "acquire", execution, label });
|
|
221
|
+
return { release: () => calls.push({ type: "release", label }) };
|
|
222
|
+
},
|
|
223
|
+
snapshot: () => ({ resources: {} })
|
|
224
|
+
};
|
|
225
|
+
const registry = new ToolRegistry({ executionGovernor });
|
|
226
|
+
await registry.load();
|
|
227
|
+
|
|
228
|
+
const result = await registry.run({ name: "heavy-tool", request: { args: {} } });
|
|
229
|
+
|
|
230
|
+
assert.equal(result.ok, true);
|
|
231
|
+
assert.deepEqual(calls, [
|
|
232
|
+
{ type: "acquire", execution: { resourceClass: "browser", weight: 1 }, label: "heavy-tool" },
|
|
233
|
+
{ type: "release", label: "heavy-tool" }
|
|
234
|
+
]);
|
|
235
|
+
});
|
|
236
|
+
|
|
196
237
|
test("runs a registered tool process with an enriched request and cleans up request files", async () => {
|
|
197
238
|
await resetHome();
|
|
198
239
|
await createFakeTool("fake-tool");
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
normalizeToolExecution,
|
|
5
|
+
normalizeToolExecutionPolicy,
|
|
6
|
+
WeightedResourceGovernor
|
|
7
|
+
} from "../src/core/tools/weighted-resource-governor.js";
|
|
8
|
+
|
|
9
|
+
function deferred() {
|
|
10
|
+
let resolve;
|
|
11
|
+
const promise = new Promise((done) => { resolve = done; });
|
|
12
|
+
return { promise, resolve };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test("normalizes manifest weights and configurable class capacities", () => {
|
|
16
|
+
assert.deepEqual(normalizeToolExecution({ resourceClass: "browser", weight: 2 }), {
|
|
17
|
+
resourceClass: "browser",
|
|
18
|
+
weight: 2
|
|
19
|
+
});
|
|
20
|
+
assert.equal(normalizeToolExecution(undefined), null);
|
|
21
|
+
assert.throws(() => normalizeToolExecution({ resourceClass: "Browser!", weight: 1 }), /Invalid tool execution resource class/);
|
|
22
|
+
assert.throws(() => normalizeToolExecution({ resourceClass: "browser", weight: 0 }), /positive integer/);
|
|
23
|
+
assert.deepEqual(normalizeToolExecutionPolicy({
|
|
24
|
+
defaultCapacity: 3,
|
|
25
|
+
maxQueuedPerClass: 12,
|
|
26
|
+
capacities: { browser: 4 }
|
|
27
|
+
}), {
|
|
28
|
+
defaultCapacity: 3,
|
|
29
|
+
maxQueuedPerClass: 12,
|
|
30
|
+
capacities: { browser: 4 }
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("queues weighted work fairly within one resource class", async () => {
|
|
35
|
+
let time = 100;
|
|
36
|
+
const governor = new WeightedResourceGovernor({
|
|
37
|
+
policy: { defaultCapacity: 2 },
|
|
38
|
+
now: () => time,
|
|
39
|
+
memoryUsage: () => ({ rss: 100 * 1024 * 1024 })
|
|
40
|
+
});
|
|
41
|
+
const execution = { resourceClass: "browser", weight: 1 };
|
|
42
|
+
const first = await governor.acquire(execution, "first");
|
|
43
|
+
const second = await governor.acquire(execution, "second");
|
|
44
|
+
const thirdLease = governor.acquire(execution, "third");
|
|
45
|
+
const fourthLease = governor.acquire(execution, "fourth");
|
|
46
|
+
|
|
47
|
+
assert.deepEqual(governor.snapshot().resources.browser, {
|
|
48
|
+
capacity: 2,
|
|
49
|
+
activeWeight: 2,
|
|
50
|
+
queued: 2
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const thirdGranted = deferred();
|
|
54
|
+
thirdLease.then((lease) => thirdGranted.resolve(lease));
|
|
55
|
+
time = 140;
|
|
56
|
+
first.release();
|
|
57
|
+
const third = await thirdGranted.promise;
|
|
58
|
+
assert.equal(third.waitedMs, 40);
|
|
59
|
+
assert.equal(governor.snapshot().resources.browser.queued, 1);
|
|
60
|
+
|
|
61
|
+
second.release();
|
|
62
|
+
const fourth = await fourthLease;
|
|
63
|
+
assert.equal(fourth.waitedMs, 40);
|
|
64
|
+
third.release();
|
|
65
|
+
fourth.release();
|
|
66
|
+
assert.equal(governor.snapshot().resources.browser.activeWeight, 0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("undeclared lightweight work bypasses constrained resource queues", async () => {
|
|
70
|
+
const governor = new WeightedResourceGovernor({ policy: { defaultCapacity: 1 } });
|
|
71
|
+
const heavy = await governor.acquire({ resourceClass: "browser", weight: 1 }, "heavy");
|
|
72
|
+
const queued = governor.acquire({ resourceClass: "browser", weight: 1 }, "queued");
|
|
73
|
+
const light = await governor.acquire(null, "light");
|
|
74
|
+
assert.equal(light.waitedMs, 0);
|
|
75
|
+
assert.equal(governor.snapshot().resources.browser.queued, 1);
|
|
76
|
+
light.release();
|
|
77
|
+
heavy.release();
|
|
78
|
+
(await queued).release();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("larger weights consume shared capacity and worker RSS peaks are retained", async () => {
|
|
82
|
+
let rss = 120 * 1024 * 1024;
|
|
83
|
+
const governor = new WeightedResourceGovernor({
|
|
84
|
+
policy: { defaultCapacity: 3 },
|
|
85
|
+
memoryUsage: () => ({ rss })
|
|
86
|
+
});
|
|
87
|
+
const large = await governor.acquire({ resourceClass: "browser", weight: 2 }, "large");
|
|
88
|
+
const waiting = governor.acquire({ resourceClass: "browser", weight: 2 }, "waiting");
|
|
89
|
+
assert.equal(governor.snapshot().resources.browser.queued, 1);
|
|
90
|
+
rss = 180 * 1024 * 1024;
|
|
91
|
+
large.release();
|
|
92
|
+
const next = await waiting;
|
|
93
|
+
assert.equal(governor.snapshot().peakRssBytes, rss);
|
|
94
|
+
next.release();
|
|
95
|
+
});
|