rol-websocket-channel 1.7.4 → 1.7.5

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.
@@ -26,9 +26,10 @@ export const getSession: MethodHandler = async (
26
26
  : undefined;
27
27
  const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
28
28
  const requestedLimit = typeof objectParams.limit === 'number' ? objectParams.limit : MAX_SESSION_MESSAGES;
29
- const requestedOffset = typeof objectParams.offset === 'number' ? objectParams.offset : 0;
29
+ const beforeId = typeof objectParams.beforeId === 'string' && objectParams.beforeId.trim()
30
+ ? objectParams.beforeId.trim()
31
+ : undefined;
30
32
  const limit = normalizePageSize(requestedLimit, MAX_SESSION_MESSAGES);
31
- const offset = normalizeOffset(requestedOffset);
32
33
 
33
34
  if (!sessionId) {
34
35
  throw new JsonRpcException(
@@ -52,7 +53,7 @@ export const getSession: MethodHandler = async (
52
53
  'sessions',
53
54
  `${session.sessionId ?? sessionId}.jsonl`
54
55
  );
55
- const messages = await readSessionMessages(sessionFile, limit, offset);
56
+ const messages = await readSessionMessages(sessionFile, limit, beforeId);
56
57
 
57
58
  return {
58
59
  agentId: session.agentId,
@@ -69,7 +70,8 @@ export const getSession: MethodHandler = async (
69
70
  messages: {
70
71
  total: messages.total,
71
72
  limit,
72
- offset,
73
+ hasMore: messages.hasMore,
74
+ nextBeforeId: messages.nextBeforeId,
73
75
  items: messages.items
74
76
  }
75
77
  };
@@ -203,21 +205,13 @@ function normalizePageSize(value: number, max: number): number {
203
205
  return Math.min(normalized, max);
204
206
  }
205
207
 
206
- function normalizeOffset(value: number): number {
207
- if (!Number.isFinite(value)) {
208
- return 0;
209
- }
210
-
211
- return Math.max(0, Math.floor(value));
212
- }
213
-
214
208
  async function readSessionMessages(
215
209
  filePath: string,
216
210
  limit: number,
217
- offset: number
218
- ): Promise<{ total: number; items: SessionMessage[] }> {
211
+ beforeId?: string
212
+ ): Promise<{ total: number; hasMore: boolean; nextBeforeId: string | null; items: SessionMessage[] }> {
219
213
  if (!(await pathExists(filePath))) {
220
- return { total: 0, items: [] };
214
+ return { total: 0, hasMore: false, nextBeforeId: null, items: [] };
221
215
  }
222
216
 
223
217
  const messages: SessionMessage[] = [];
@@ -239,9 +233,24 @@ async function readSessionMessages(
239
233
  }
240
234
  }
241
235
 
236
+ const endExclusive = beforeId ? messages.findIndex((message) => message.id === beforeId) : messages.length;
237
+ if (endExclusive < 0) {
238
+ throw new JsonRpcException(
239
+ JSON_RPC_ERRORS.invalidParams,
240
+ `beforeId not found: ${beforeId}`
241
+ );
242
+ }
243
+
244
+ const start = Math.max(0, endExclusive - limit);
245
+ const items = messages.slice(start, endExclusive);
246
+ const hasMore = start > 0;
247
+ const firstItemId = typeof items[0]?.id === 'string' ? items[0].id : null;
248
+
242
249
  return {
243
250
  total: messages.length,
244
- items: messages.slice(offset, offset + limit)
251
+ hasMore,
252
+ nextBeforeId: hasMore ? firstItemId : null,
253
+ items
245
254
  };
246
255
  }
247
256
 
package/tsconfig.json CHANGED
@@ -13,5 +13,5 @@
13
13
  "rootDir": "."
14
14
  },
15
15
  "include": ["index.ts", "src/**/*.ts", "types/**/*.d.ts"],
16
- "exclude": ["node_modules", "dist"]
16
+ "exclude": ["node_modules", "dist", "test", "**/*.test.ts", "**/*.spec.ts"]
17
17
  }
@@ -1,122 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { readFileSync } from 'node:fs';
3
- import test from 'node:test';
4
- import entry, { formatCliErrorPayload } from '../../index.js';
5
- const manifest = JSON.parse(readFileSync(new URL('../../openclaw.plugin.json', import.meta.url), 'utf8'));
6
- const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
7
- test('manifest declares OpenClaw 2026 command ownership for admin bridge CLI', () => {
8
- assert.deepEqual(manifest.activation?.onCommands, ['admin-bridge', 'rol-websocket-channel']);
9
- assert.deepEqual(manifest.commandAliases, [
10
- { name: 'admin-bridge' },
11
- { name: 'rol-websocket-channel' }
12
- ]);
13
- assert.deepEqual(manifest.contracts?.tools, [
14
- 'rol_artifacts_find_latest',
15
- 'rol_artifacts_publish'
16
- ]);
17
- });
18
- test('package.json declares official OpenClaw runtime metadata', () => {
19
- assert.deepEqual(packageJson.openclaw?.extensions, ['./index.ts']);
20
- assert.deepEqual(packageJson.openclaw?.runtimeExtensions, ['./dist/index.js']);
21
- assert.deepEqual(packageJson.openclaw?.compat, {
22
- pluginApi: '>=2026.5.7',
23
- minGatewayVersion: '2026.5.7'
24
- });
25
- assert.deepEqual(packageJson.openclaw?.build, {
26
- openclawVersion: '2026.5.7',
27
- pluginSdkVersion: '2026.5.7'
28
- });
29
- });
30
- test('runtime entry exposes admin-bridge CLI roots and registers artifacts tools in full mode', () => {
31
- const descriptors = [];
32
- const commands = [];
33
- const aliases = [];
34
- const registeredTools = [];
35
- const commandNode = {
36
- alias(name) {
37
- aliases.push(name);
38
- return commandNode;
39
- },
40
- description() {
41
- return commandNode;
42
- },
43
- addHelpText() {
44
- return commandNode;
45
- },
46
- option() {
47
- return commandNode;
48
- },
49
- action() {
50
- return commandNode;
51
- },
52
- command(name) {
53
- commands.push(name);
54
- return commandNode;
55
- }
56
- };
57
- const api = {
58
- registrationMode: 'full',
59
- runtime: {},
60
- registerChannel() { },
61
- registerTool(tool) {
62
- registeredTools.push(tool);
63
- },
64
- registerCli(callback, options) {
65
- descriptors.push(...options.descriptors);
66
- callback({ program: commandNode });
67
- }
68
- };
69
- entry.register(api);
70
- assert.deepEqual(descriptors, [
71
- {
72
- name: 'admin-bridge',
73
- description: 'OpenClaw admin bridge commands',
74
- hasSubcommands: true
75
- },
76
- {
77
- name: 'rol-websocket-channel',
78
- description: 'OpenClaw admin bridge commands',
79
- hasSubcommands: true
80
- }
81
- ]);
82
- assert.equal(commands[0], 'admin-bridge');
83
- assert.equal(aliases[0], 'rol-websocket-channel');
84
- assert.equal(registeredTools.length, 1);
85
- });
86
- test('CLI metadata mode skips full-only tool registration', () => {
87
- let registerToolCalled = false;
88
- const api = {
89
- registrationMode: 'cli-metadata',
90
- runtime: {},
91
- registerChannel() { },
92
- registerTool() {
93
- registerToolCalled = true;
94
- },
95
- registerCli() { }
96
- };
97
- entry.register(api);
98
- assert.equal(registerToolCalled, false);
99
- });
100
- test('CLI error formatter preserves diagnostic data for pairing failures', () => {
101
- const error = new Error('mqttUrl is missing from pairing payload');
102
- error.data = {
103
- code: 'PAIR_CHANNEL_CONFIG_INVALID',
104
- debug: {
105
- rootKeys: ['channel'],
106
- channelConfigKeys: ['mqtt_topic'],
107
- hasExistingMqttUrl: false
108
- }
109
- };
110
- assert.deepEqual(formatCliErrorPayload(error), {
111
- ok: false,
112
- error: {
113
- message: 'mqttUrl is missing from pairing payload',
114
- code: 'PAIR_CHANNEL_CONFIG_INVALID',
115
- debug: {
116
- rootKeys: ['channel'],
117
- channelConfigKeys: ['mqtt_topic'],
118
- hasExistingMqttUrl: false
119
- }
120
- }
121
- });
122
- });
@@ -1,37 +0,0 @@
1
- import { describe, test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import fs from 'node:fs/promises';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import { resolveOpenClawBin } from './openclaw-bin.js';
7
- describe('resolveOpenClawBin', () => {
8
- test('uses APPDATA npm shim on Windows when OPENCLAW_BIN is unset', { skip: process.platform !== 'win32' }, async () => {
9
- const originalOpenClawBin = process.env.OPENCLAW_BIN;
10
- const originalAppData = process.env.APPDATA;
11
- const root = await fs.mkdtemp(path.join(os.tmpdir(), 'openclaw-bin-'));
12
- try {
13
- const appData = path.join(root, 'Roaming');
14
- const shim = path.join(appData, 'npm', 'openclaw.cmd');
15
- await fs.mkdir(path.dirname(shim), { recursive: true });
16
- await fs.writeFile(shim, '@echo off\r\n', 'utf8');
17
- delete process.env.OPENCLAW_BIN;
18
- process.env.APPDATA = appData;
19
- assert.equal(resolveOpenClawBin(), shim);
20
- }
21
- finally {
22
- if (originalOpenClawBin === undefined) {
23
- delete process.env.OPENCLAW_BIN;
24
- }
25
- else {
26
- process.env.OPENCLAW_BIN = originalOpenClawBin;
27
- }
28
- if (originalAppData === undefined) {
29
- delete process.env.APPDATA;
30
- }
31
- else {
32
- process.env.APPDATA = originalAppData;
33
- }
34
- await fs.rm(root, { recursive: true, force: true });
35
- }
36
- });
37
- });
@@ -1,61 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import fs from 'node:fs/promises';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import test from 'node:test';
6
- import { setApiCoreBotConfig } from './admin.js';
7
- test('setApiCoreBotConfig writes apiCoreBot endpoint without changing pairing or channel config', async () => {
8
- const context = await createMethodContext();
9
- const configPath = path.join(context.openclawRoot, 'openclaw.json');
10
- await fs.writeFile(configPath, JSON.stringify({
11
- plugins: {
12
- entries: {
13
- 'rol-websocket-channel': {
14
- enabled: true,
15
- config: {
16
- pairing: {
17
- paired: true,
18
- pairingKeyLast4: '7a46'
19
- }
20
- }
21
- }
22
- }
23
- },
24
- channels: {
25
- 'rol-websocket-channel': {
26
- enabled: true,
27
- config: {
28
- mqttUrl: 'ws://mqtt.example.test:8083/mqtt'
29
- }
30
- }
31
- }
32
- }, null, 2));
33
- const result = await setApiCoreBotConfig({
34
- baseUrl: 'https://api.example.test/',
35
- authToken: 'secret-token'
36
- }, context);
37
- const savedConfig = JSON.parse(await fs.readFile(configPath, 'utf8'));
38
- assert.equal(result.ok, true);
39
- assert.equal(result.apiCoreBot.baseUrl, 'https://api.example.test');
40
- assert.equal(result.apiCoreBot.authToken, 'secr***oken');
41
- assert.equal(savedConfig.plugins.entries['rol-websocket-channel'].config.apiCoreBot.baseUrl, 'https://api.example.test');
42
- assert.equal(savedConfig.plugins.entries['rol-websocket-channel'].config.apiCoreBot.authToken, 'secret-token');
43
- assert.equal(savedConfig.plugins.entries['rol-websocket-channel'].config.pairing.pairingKeyLast4, '7a46');
44
- assert.equal(savedConfig.channels['rol-websocket-channel'].config.mqttUrl, 'ws://mqtt.example.test:8083/mqtt');
45
- });
46
- test('setApiCoreBotConfig rejects missing baseUrl instead of writing defaults', async () => {
47
- const context = await createMethodContext();
48
- const configPath = path.join(context.openclawRoot, 'openclaw.json');
49
- await fs.writeFile(configPath, '{}');
50
- await assert.rejects(() => setApiCoreBotConfig({}, context), /apiCoreBot\.baseUrl is required/);
51
- assert.equal(await fs.readFile(configPath, 'utf8'), '{}');
52
- });
53
- async function createMethodContext() {
54
- const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'api-core-bot-config-test-'));
55
- const openclawRoot = path.join(projectRoot, '.openclaw');
56
- await fs.mkdir(openclawRoot, { recursive: true });
57
- return {
58
- projectRoot,
59
- openclawRoot
60
- };
61
- }
@@ -1,304 +0,0 @@
1
- import { afterEach, describe, test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import fs from 'node:fs/promises';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import * as artifactMethods from './artifacts.js';
7
- import { ensureArtifactUploaded, listArtifacts, markArtifactUploaded, refreshArtifacts } from './artifacts.js';
8
- const tempDirs = [];
9
- const originalFetch = globalThis.fetch;
10
- const optionalArtifactMethods = artifactMethods;
11
- afterEach(async () => {
12
- globalThis.fetch = originalFetch;
13
- while (tempDirs.length > 0) {
14
- const dir = tempDirs.pop();
15
- if (dir) {
16
- await fs.rm(dir, { recursive: true, force: true });
17
- }
18
- }
19
- });
20
- describe('artifacts workspace scope', () => {
21
- test('only indexes allowed artifact types inside workspace', async () => {
22
- const context = await createMethodContext();
23
- const workspaceRoot = path.join(context.openclawRoot, 'workspace');
24
- await fs.writeFile(path.join(workspaceRoot, 'SOUL.md'), '# root noise\n', 'utf8');
25
- await fs.mkdir(path.join(workspaceRoot, 'exports'), { recursive: true });
26
- await fs.mkdir(path.join(workspaceRoot, 'logs'), { recursive: true });
27
- await fs.mkdir(path.join(workspaceRoot, 'nested', 'media'), { recursive: true });
28
- await fs.writeFile(path.join(workspaceRoot, 'exports', 'preview.png'), 'png-data', 'utf8');
29
- await fs.writeFile(path.join(workspaceRoot, 'exports', 'report.pdf'), 'pdf-data', 'utf8');
30
- await fs.writeFile(path.join(workspaceRoot, 'nested', 'media', 'archive.zip'), 'zip-data', 'utf8');
31
- await fs.writeFile(path.join(workspaceRoot, 'nested', 'media', 'video.mp4'), 'mp4-data', 'utf8');
32
- await fs.writeFile(path.join(workspaceRoot, 'exports', 'spec.docx'), 'docx-data', 'utf8');
33
- await fs.writeFile(path.join(workspaceRoot, 'exports', 'ignored.md'), '# markdown noise\n', 'utf8');
34
- await fs.writeFile(path.join(workspaceRoot, 'exports', 'ignored.json'), '{"noise":true}\n', 'utf8');
35
- await fs.writeFile(path.join(workspaceRoot, 'logs', 'ignored.pdf'), 'log pdf noise', 'utf8');
36
- await fs.writeFile(path.join(workspaceRoot, 'artifacts.json'), '[]', 'utf8');
37
- const result = await refreshArtifacts({}, context);
38
- assert.equal(result.scope, 'workspace');
39
- assert.equal(result.count, 5);
40
- assert.deepEqual(result.items.map((item) => item.relativePath), [
41
- 'nested/media/archive.zip',
42
- 'exports/preview.png',
43
- 'exports/report.pdf',
44
- 'exports/spec.docx',
45
- 'nested/media/video.mp4'
46
- ]);
47
- assert.equal(result.workspaceRoot, workspaceRoot);
48
- assert.equal(result.manifestPath, path.join(workspaceRoot, 'artifacts.json'));
49
- });
50
- test('indexes only markdown files added after the initial baseline', async () => {
51
- const context = await createMethodContext();
52
- const workspaceRoot = path.join(context.openclawRoot, 'workspace');
53
- await fs.mkdir(path.join(workspaceRoot, 'docs'), { recursive: true });
54
- await fs.writeFile(path.join(workspaceRoot, 'docs', 'existing.md'), '# existing\n', 'utf8');
55
- await fs.writeFile(path.join(workspaceRoot, 'MEMORY.md'), '# memory\n', 'utf8');
56
- await fs.writeFile(path.join(workspaceRoot, 'SoUL.md'), '# soul\n', 'utf8');
57
- const baseline = await refreshArtifacts({}, context);
58
- assert.equal(baseline.count, 0);
59
- assert.deepEqual(baseline.items, []);
60
- await fs.writeFile(path.join(workspaceRoot, 'docs', 'existing.md'), '# changed\n', 'utf8');
61
- await fs.writeFile(path.join(workspaceRoot, 'MEMORY.md'), '# changed memory\n', 'utf8');
62
- await fs.writeFile(path.join(workspaceRoot, 'docs', 'generated.md'), '# generated\n', 'utf8');
63
- const refreshed = await refreshArtifacts({}, context);
64
- assert.equal(refreshed.count, 1);
65
- assert.deepEqual(refreshed.items.map((item) => item.relativePath), ['docs/generated.md']);
66
- assert.equal(refreshed.items[0]?.category, 'document');
67
- assert.equal(refreshed.items[0]?.mimeType, 'text/markdown');
68
- });
69
- test('list reads workspace manifest without taskId', async () => {
70
- const context = await createMethodContext();
71
- const workspaceRoot = path.join(context.openclawRoot, 'workspace');
72
- await fs.mkdir(path.join(workspaceRoot, 'exports'), { recursive: true });
73
- await fs.writeFile(path.join(workspaceRoot, 'exports', 'me.pdf'), 'pdf-data', 'utf8');
74
- const refreshed = await refreshArtifacts({}, context);
75
- assert.equal(refreshed.count, 1);
76
- const listed = await listArtifacts({ refresh: false }, context);
77
- assert.equal(listed.scope, 'workspace');
78
- assert.equal(listed.count, 1);
79
- assert.deepEqual(listed.items.map((item) => item.relativePath), ['exports/me.pdf']);
80
- assert.equal(listed.manifestPath, path.join(workspaceRoot, 'artifacts.json'));
81
- assert.equal(listed.workspaceRoot, workspaceRoot);
82
- });
83
- test('ensureUploaded uploads local artifact and updates manifest', async () => {
84
- const context = await createMethodContext();
85
- const workspaceRoot = path.join(context.openclawRoot, 'workspace');
86
- const artifactPath = path.join(workspaceRoot, 'exports', 'me.pdf');
87
- await writeApiCoreBotConfig(context.openclawRoot, 'https://api.example.com');
88
- await fs.mkdir(path.dirname(artifactPath), { recursive: true });
89
- await fs.writeFile(artifactPath, 'pdf-data', 'utf8');
90
- const refreshed = await refreshArtifacts({}, context);
91
- const artifactId = refreshed.items[0]?.id;
92
- assert.ok(artifactId);
93
- const calls = [];
94
- globalThis.fetch = (async (input, init) => {
95
- const url = typeof input === 'string'
96
- ? input
97
- : input instanceof URL
98
- ? input.toString()
99
- : input.url;
100
- const method = init?.method ?? (input instanceof Request ? input.method : 'GET');
101
- calls.push({ url, method });
102
- if (url === 'https://api.example.com/api-core-bot/front/s3/get-presigned-post') {
103
- assert.equal(init?.body !== undefined, true);
104
- const parsedBody = JSON.parse(String(init?.body));
105
- assert.equal(parsedBody.dir, 'artifacts/');
106
- assert.equal(parsedBody.filename, 'me.pdf');
107
- assert.equal(parsedBody.file_name, undefined);
108
- return new Response(JSON.stringify({
109
- code: 0,
110
- message: '',
111
- success: true,
112
- data: {
113
- url: 'https://upload.example.com',
114
- fields: {
115
- key: 'artifacts/me.pdf',
116
- policy: 'policy-token'
117
- },
118
- file_key: 'artifacts/me.pdf',
119
- file_url: 'https://cdn.example.com/artifacts/me.pdf'
120
- }
121
- }), {
122
- status: 200,
123
- headers: { 'Content-Type': 'application/json' }
124
- });
125
- }
126
- if (url === 'https://upload.example.com') {
127
- assert.ok(init?.body instanceof FormData);
128
- return new Response(null, { status: 204 });
129
- }
130
- throw new Error(`Unexpected fetch call: ${url}`);
131
- });
132
- const ensured = await ensureArtifactUploaded({
133
- artifactId,
134
- presignedPostBody: {
135
- dir: 'artifacts/'
136
- }
137
- }, context);
138
- assert.equal(ensured.ok, true);
139
- assert.equal(ensured.uploaded, true);
140
- assert.equal(ensured.downloadUrl, 'https://cdn.example.com/artifacts/me.pdf');
141
- assert.equal(ensured.objectKey, 'artifacts/me.pdf');
142
- assert.equal(ensured.item.storageStatus, 'uploaded');
143
- assert.equal(ensured.item.fileUrl, 'https://cdn.example.com/artifacts/me.pdf');
144
- assert.equal(ensured.item.objectKey, 'artifacts/me.pdf');
145
- assert.deepEqual(calls, [
146
- {
147
- url: 'https://api.example.com/api-core-bot/front/s3/get-presigned-post',
148
- method: 'POST'
149
- },
150
- {
151
- url: 'https://upload.example.com',
152
- method: 'POST'
153
- }
154
- ]);
155
- const listed = await listArtifacts({ refresh: false }, context);
156
- assert.equal(listed.items.length, 1);
157
- assert.equal(listed.items[0]?.relativePath, 'exports/me.pdf');
158
- assert.equal(listed.items[0]?.storageStatus, 'uploaded');
159
- assert.equal(listed.items[0]?.fileUrl, 'https://cdn.example.com/artifacts/me.pdf');
160
- assert.equal(listed.items[0]?.objectKey, 'artifacts/me.pdf');
161
- });
162
- test('ensureUploaded reuses existing uploaded artifact without fetching', async () => {
163
- const context = await createMethodContext();
164
- const workspaceRoot = path.join(context.openclawRoot, 'workspace');
165
- const artifactPath = path.join(workspaceRoot, 'exports', 'me.pdf');
166
- await fs.mkdir(path.dirname(artifactPath), { recursive: true });
167
- await fs.writeFile(artifactPath, 'pdf-data', 'utf8');
168
- const refreshed = await refreshArtifacts({}, context);
169
- const artifactId = refreshed.items[0]?.id;
170
- assert.ok(artifactId);
171
- await markArtifactUploaded({
172
- artifactId,
173
- objectKey: 'artifacts/me.pdf',
174
- fileUrl: 'https://cdn.example.com/artifacts/me.pdf'
175
- }, context);
176
- globalThis.fetch = (async () => {
177
- throw new Error('fetch should not be called for existing uploaded artifact');
178
- });
179
- const ensured = await ensureArtifactUploaded({ artifactId }, context);
180
- assert.equal(ensured.ok, true);
181
- assert.equal(ensured.uploaded, false);
182
- assert.equal(ensured.downloadUrl, 'https://cdn.example.com/artifacts/me.pdf');
183
- assert.equal(ensured.item.storageStatus, 'uploaded');
184
- assert.equal(ensured.item.fileUrl, 'https://cdn.example.com/artifacts/me.pdf');
185
- });
186
- test('findLatest returns newest matching artifacts with filters', async () => {
187
- const findLatestArtifacts = optionalArtifactMethods.findLatestArtifacts;
188
- if (!findLatestArtifacts) {
189
- return;
190
- }
191
- const context = await createMethodContext();
192
- const workspaceRoot = path.join(context.openclawRoot, 'workspace');
193
- await fs.mkdir(path.join(workspaceRoot, 'exports'), { recursive: true });
194
- await fs.writeFile(path.join(workspaceRoot, 'exports', 'first.pdf'), 'pdf-data', 'utf8');
195
- await new Promise((resolve) => setTimeout(resolve, 20));
196
- await fs.writeFile(path.join(workspaceRoot, 'exports', 'second.png'), 'png-data', 'utf8');
197
- await new Promise((resolve) => setTimeout(resolve, 20));
198
- await fs.writeFile(path.join(workspaceRoot, 'exports', 'third.docx'), 'docx-data', 'utf8');
199
- const latest = await findLatestArtifacts({
200
- category: 'document',
201
- relativePathPrefix: 'exports/',
202
- limit: 2
203
- }, context);
204
- assert.equal(latest.ok, true);
205
- assert.equal(latest.count, 2);
206
- assert.deepEqual(latest.items.map((item) => item.fileName), ['third.docx', 'first.pdf']);
207
- assert.deepEqual(latest.items.map((item) => item.category), ['document', 'document']);
208
- assert.deepEqual(latest.items.map((item) => item.relativePath), ['exports/third.docx', 'exports/first.pdf']);
209
- });
210
- test('publish refreshes manifest for a new file and uploads it by relativePath', async () => {
211
- const publishArtifact = optionalArtifactMethods.publishArtifact;
212
- if (!publishArtifact) {
213
- return;
214
- }
215
- const context = await createMethodContext();
216
- const workspaceRoot = path.join(context.openclawRoot, 'workspace');
217
- const artifactPath = path.join(workspaceRoot, 'generated', 'result.pdf');
218
- await writeApiCoreBotConfig(context.openclawRoot, 'https://api.example.com');
219
- await fs.mkdir(path.dirname(artifactPath), { recursive: true });
220
- await fs.writeFile(artifactPath, 'pdf-data', 'utf8');
221
- const calls = [];
222
- globalThis.fetch = (async (input, init) => {
223
- const url = typeof input === 'string'
224
- ? input
225
- : input instanceof URL
226
- ? input.toString()
227
- : input.url;
228
- const method = init?.method ?? (input instanceof Request ? input.method : 'GET');
229
- calls.push({ url, method });
230
- if (url === 'https://api.example.com/api-core-bot/front/s3/get-presigned-post') {
231
- const parsedBody = JSON.parse(String(init?.body));
232
- assert.equal(parsedBody.filename, 'result.pdf');
233
- assert.equal(parsedBody.dir, 'generated/');
234
- return new Response(JSON.stringify({
235
- success: true,
236
- data: {
237
- url: 'https://upload.example.com',
238
- fields: {
239
- key: 'generated/result.pdf',
240
- policy: 'policy-token'
241
- },
242
- file_url: 'https://cdn.example.com/generated/result.pdf'
243
- }
244
- }), {
245
- status: 200,
246
- headers: { 'Content-Type': 'application/json' }
247
- });
248
- }
249
- if (url === 'https://upload.example.com') {
250
- assert.ok(init?.body instanceof FormData);
251
- return new Response(null, { status: 204 });
252
- }
253
- throw new Error(`Unexpected fetch call: ${url}`);
254
- });
255
- const published = await publishArtifact({
256
- relativePath: 'generated/result.pdf',
257
- presignedPostBody: {
258
- dir: 'generated/'
259
- }
260
- }, context);
261
- assert.equal(published.ok, true);
262
- assert.equal(published.uploaded, true);
263
- assert.equal(published.downloadUrl, 'https://cdn.example.com/generated/result.pdf');
264
- assert.equal(published.item.relativePath, 'generated/result.pdf');
265
- assert.equal(published.item.storageStatus, 'uploaded');
266
- assert.match(published.artifactId, /^art_/);
267
- assert.deepEqual(calls, [
268
- {
269
- url: 'https://api.example.com/api-core-bot/front/s3/get-presigned-post',
270
- method: 'POST'
271
- },
272
- {
273
- url: 'https://upload.example.com',
274
- method: 'POST'
275
- }
276
- ]);
277
- });
278
- });
279
- async function createMethodContext() {
280
- const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'artifacts-task-scope-'));
281
- tempDirs.push(rootDir);
282
- const openclawRoot = path.join(rootDir, '.openclaw');
283
- const workspaceRoot = path.join(openclawRoot, 'workspace');
284
- await fs.mkdir(workspaceRoot, { recursive: true });
285
- return {
286
- projectRoot: rootDir,
287
- openclawRoot
288
- };
289
- }
290
- async function writeApiCoreBotConfig(openclawRoot, baseUrl) {
291
- await fs.writeFile(path.join(openclawRoot, 'openclaw.json'), JSON.stringify({
292
- plugins: {
293
- entries: {
294
- 'rol-websocket-channel': {
295
- config: {
296
- apiCoreBot: {
297
- baseUrl
298
- }
299
- }
300
- }
301
- }
302
- }
303
- }, null, 2), 'utf8');
304
- }
@@ -1,27 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
- import * as artifactMethods from './artifacts.js';
4
- import { setApiCoreBotConfig } from './admin.js';
5
- import { getMethod, listMethods } from './index.js';
6
- import { currentVersion } from './system.js';
7
- test('methods registry exposes local artifact discovery and publish methods', () => {
8
- const optionalArtifactMethods = artifactMethods;
9
- if (optionalArtifactMethods.findLatestArtifacts) {
10
- assert.equal(getMethod('artifacts.findLatest'), optionalArtifactMethods.findLatestArtifacts);
11
- assert.ok(listMethods().includes('artifacts.findLatest'));
12
- }
13
- if (optionalArtifactMethods.publishArtifact) {
14
- assert.equal(getMethod('artifacts.publish'), optionalArtifactMethods.publishArtifact);
15
- assert.ok(listMethods().includes('artifacts.publish'));
16
- }
17
- });
18
- test('methods registry exposes apiCoreBot config writer', () => {
19
- assert.equal(getMethod('config.setApiCoreBot'), setApiCoreBotConfig);
20
- assert.ok(listMethods().includes('config.setApiCoreBot'));
21
- });
22
- test('methods registry exposes current version method', () => {
23
- assert.equal(getMethod('system.currentVersion'), currentVersion);
24
- assert.ok(listMethods().includes('system.currentVersion'));
25
- assert.ok(!listMethods().includes('system.ensureLatest'));
26
- assert.ok(!listMethods().includes('config.setVersionManagement'));
27
- });