neoctl-web 0.1.14 → 0.1.16
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 +34 -256
- package/dist/assets/App-Bn6HVDCw.js +105 -0
- package/dist/assets/{index-Ckm5fgYu.js → index-BkoW_zgA.js} +2 -2
- package/dist/index.html +1 -1
- package/package.json +14 -13
- package/plugins/downloads/README.md +1 -1
- package/plugins/video-share/README.md +1 -1
- package/server.mjs +1 -42
- package/control-protocol.mjs +0 -44
- package/control-sync.mjs +0 -420
- package/dist/assets/App--pG9o2Yv.js +0 -105
- package/plugins/downloads/downloads.test.mjs +0 -83
- package/plugins/video-share/video-share.test.mjs +0 -182
- package/plugins/xhs-artifact/version.test.mjs +0 -151
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
import http from 'node:http';
|
|
7
|
-
import { spawnSync } from 'node:child_process';
|
|
8
|
-
import { createPlugin } from './index.mjs';
|
|
9
|
-
|
|
10
|
-
async function setup(t) {
|
|
11
|
-
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'downloads-test-'));
|
|
12
|
-
const outside = await fs.mkdtemp(path.join(os.tmpdir(), 'outside-workspace-'));
|
|
13
|
-
const source = path.join(outside, '中文 [report].txt');
|
|
14
|
-
await fs.writeFile(source, 'original contents');
|
|
15
|
-
const context = { appDataDir: root, env: {} };
|
|
16
|
-
let plugin = createPlugin(context);
|
|
17
|
-
const server = http.createServer(async (req, res) => {
|
|
18
|
-
try { if (!await plugin.route(req, res, new URL(req.url, 'http://localhost'))) { res.statusCode = 404; res.end(); } }
|
|
19
|
-
catch (error) { res.statusCode = 500; res.end(error.message); }
|
|
20
|
-
});
|
|
21
|
-
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
22
|
-
t.after(async () => { server.closeAllConnections(); await new Promise((r) => server.close(r)); await fs.rm(root, { recursive: true, force: true }); await fs.rm(outside, { recursive: true, force: true }); });
|
|
23
|
-
return { root, outside, source, plugin, base: `http://127.0.0.1:${server.address().port}`, restart() { plugin = createPlugin(context); } };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
test('arbitrary outside-workspace file: only metadata stored, no copies, no expiry', async (t) => {
|
|
27
|
-
const f = await setup(t);
|
|
28
|
-
const result = await f.plugin.tools[0].execute({ paths: [f.source] }, { cwd: f.root });
|
|
29
|
-
assert.equal(result.ok, true);
|
|
30
|
-
const d = result.output.downloads[0];
|
|
31
|
-
assert.equal(d.expiresAt, null); assert.equal(d.expiresAtEpochMs, null);
|
|
32
|
-
assert.equal(result.output._ui.resources[0].expiresAt, undefined);
|
|
33
|
-
const storedDir = path.join(f.root, 'downloads', d.id);
|
|
34
|
-
assert.deepEqual(await fs.readdir(storedDir), ['entry.json']);
|
|
35
|
-
const record = JSON.parse(await fs.readFile(path.join(storedDir, 'entry.json'), 'utf8'));
|
|
36
|
-
assert.equal(record.absolutePath, f.source);
|
|
37
|
-
const r = await fetch(f.base + d.url);
|
|
38
|
-
assert.equal(r.status, 200); assert.match(r.headers.get('content-disposition'), /^attachment;/);
|
|
39
|
-
assert.equal(await r.text(), 'original contents');
|
|
40
|
-
const head = await fetch(f.base + d.url, { method: 'HEAD' });
|
|
41
|
-
assert.equal(head.status, 200); assert.equal(head.headers.get('content-length'), '17'); assert.equal(await head.text(), '');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('mapping survives restart and future clock; serves current original bytes and length', async (t) => {
|
|
45
|
-
const f = await setup(t), d = (await f.plugin.tools[0].execute({ paths: [f.source] })).output.downloads[0];
|
|
46
|
-
const script = `import { DownloadRegistry } from ${JSON.stringify(new URL('./downloads.mjs', import.meta.url).href)}; Date.now = () => 9999999999999; const e = await new DownloadRegistry({ storageDir: ${JSON.stringify(path.join(f.root, 'downloads'))} }).get(${JSON.stringify(d.id)}); if (!e || e.expiresAt !== null) process.exit(1); console.log(e.id);`;
|
|
47
|
-
const child = spawnSync(process.execPath, ['--input-type=module', '-e', script], { encoding: 'utf8' });
|
|
48
|
-
assert.equal(child.status, 0, child.stderr);
|
|
49
|
-
f.restart();
|
|
50
|
-
await fs.writeFile(f.source, 'updated longer original contents');
|
|
51
|
-
const r = await fetch(f.base + d.url); assert.equal(r.status, 200); assert.equal(r.headers.get('content-length'), '32'); assert.equal(await r.text(), 'updated longer original contents');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('moving or deleting the original invalidates the link, no fallback copy', async (t) => {
|
|
55
|
-
const f = await setup(t), d = (await f.plugin.tools[0].execute({ paths: [f.source] })).output.downloads[0];
|
|
56
|
-
const moved = path.join(f.outside, 'moved.txt');
|
|
57
|
-
await fs.rename(f.source, moved);
|
|
58
|
-
const missing = await fetch(f.base + d.url); assert.equal(missing.status, 404); await missing.text();
|
|
59
|
-
f.restart(); const afterRestart = await fetch(f.base + d.url); assert.equal(afterRestart.status, 404); await afterRestart.text();
|
|
60
|
-
const next = (await f.plugin.tools[0].execute({ paths: [moved] })).output.downloads[0];
|
|
61
|
-
await fs.rm(moved);
|
|
62
|
-
const deleted = await fetch(f.base + next.url); assert.equal(deleted.status, 404); await deleted.text();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('invalid paths, directories, missing files, malformed URLs and HTTP writes are rejected', async (t) => {
|
|
66
|
-
const f = await setup(t), tool = f.plugin.tools[0];
|
|
67
|
-
for (const paths of [[], ['relative'], [null], Array(21).fill(f.source)]) assert.throws(() => tool.validate({ paths }));
|
|
68
|
-
const result = await tool.execute({ paths: [f.source, f.outside, path.join(f.outside, 'absent')] });
|
|
69
|
-
assert.equal(result.ok, false); assert.equal(result.output.errors.length, 2); assert.equal(result.output.downloads.length, 1);
|
|
70
|
-
const d = result.output.downloads[0];
|
|
71
|
-
for (const suffix of ['%2e%2e%2fsecret', 'bad%', 'a'.repeat(36), d.id + '/extra']) {
|
|
72
|
-
const r = await fetch(f.base + '/api/downloads/' + suffix); assert.equal(r.status, 404); await r.text();
|
|
73
|
-
}
|
|
74
|
-
const post = await fetch(f.base + d.url, { method: 'POST' }); assert.equal(post.status, 405); await post.text();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('concurrent exposure persists separate tiny mappings', async (t) => {
|
|
78
|
-
const f = await setup(t);
|
|
79
|
-
const results = await Promise.all(Array.from({ length: 8 }, () => f.plugin.tools[0].execute({ paths: [f.source] })));
|
|
80
|
-
const ids = results.map((r) => r.output.downloads[0].id);
|
|
81
|
-
assert.equal(new Set(ids).size, 8);
|
|
82
|
-
for (const id of ids) assert.deepEqual(await fs.readdir(path.join(f.root, 'downloads', id)), ['entry.json']);
|
|
83
|
-
});
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import 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 http from 'node:http';
|
|
7
|
-
import { spawnSync } from 'node:child_process';
|
|
8
|
-
import { createPlugin } from './index.mjs';
|
|
9
|
-
import { VideoStore } from './store.mjs';
|
|
10
|
-
import { parseRange, playerPage } from './http.mjs';
|
|
11
|
-
|
|
12
|
-
async function setup(t) {
|
|
13
|
-
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'video-share-test-'));
|
|
14
|
-
const source = path.join(root, '样例 video.mp4');
|
|
15
|
-
const bytes = Buffer.concat([Buffer.from([0, 0, 0, 24]), Buffer.from('ftypisom'), Buffer.alloc(1024, 7)]);
|
|
16
|
-
await fs.writeFile(source, bytes);
|
|
17
|
-
const context = { appDataDir: path.join(root, 'data'), env: {} };
|
|
18
|
-
let plugin = createPlugin(context);
|
|
19
|
-
const server = http.createServer(async (req, res) => {
|
|
20
|
-
try { if (!await plugin.route(req, res, new URL(req.url, 'http://localhost'))) { res.statusCode = 404; res.end(); } }
|
|
21
|
-
catch (error) { res.statusCode = 500; res.end(error.message); }
|
|
22
|
-
});
|
|
23
|
-
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
24
|
-
t.after(async () => { server.closeAllConnections(); await new Promise((r) => server.close(r)); await fs.rm(root, { recursive: true, force: true }); });
|
|
25
|
-
const base = `http://127.0.0.1:${server.address().port}`;
|
|
26
|
-
return { root, source, bytes, context, base, plugin, restart() { plugin = createPlugin(context); return plugin; } };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function expose(plugin, paths) { return plugin.tools.find((t) => t.name === 'expose_videos').execute({ paths }); }
|
|
30
|
-
|
|
31
|
-
test('standalone publication, player, inline media and HEAD work without host imports', async (t) => {
|
|
32
|
-
const f = await setup(t);
|
|
33
|
-
const result = await expose(f.plugin, [f.source]);
|
|
34
|
-
assert.equal(result.ok, true);
|
|
35
|
-
const v = result.output.videos[0];
|
|
36
|
-
assert.equal(v.expiresAt, null);
|
|
37
|
-
assert.equal(result.output._ui.resources[0].kind, 'embed');
|
|
38
|
-
assert.equal(result.output._ui.presentationLevel, 'primary');
|
|
39
|
-
assert.equal(v.markdown, undefined);
|
|
40
|
-
assert.equal(v.reference, undefined);
|
|
41
|
-
assert.equal(result.output._ui.resources[0].downloadName, undefined);
|
|
42
|
-
const page = await fetch(f.base + v.url);
|
|
43
|
-
assert.equal(page.status, 200);
|
|
44
|
-
assert.match(page.headers.get('content-security-policy'), /default-src 'none'/);
|
|
45
|
-
assert.match(await page.text(), /<video controls playsinline preload="metadata"/);
|
|
46
|
-
const media = await fetch(f.base + v.mediaUrl);
|
|
47
|
-
assert.equal(media.headers.get('content-type'), 'video/mp4');
|
|
48
|
-
assert.match(media.headers.get('content-disposition'), /^inline;/);
|
|
49
|
-
assert.equal(media.headers.get('accept-ranges'), 'bytes');
|
|
50
|
-
assert.deepEqual(Buffer.from(await media.arrayBuffer()), f.bytes);
|
|
51
|
-
const head = await fetch(f.base + v.mediaUrl, { method: 'HEAD' });
|
|
52
|
-
assert.equal(head.status, 200);
|
|
53
|
-
assert.equal(Number(head.headers.get('content-length')), f.bytes.length);
|
|
54
|
-
assert.equal((await head.arrayBuffer()).byteLength, 0);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('byte range seeking: bounded, open-ended, suffix, clamped, invalid, If-Range', async (t) => {
|
|
58
|
-
const f = await setup(t), v = (await expose(f.plugin, [f.source])).output.videos[0];
|
|
59
|
-
for (const [range, start, end] of [['bytes=2-9', 2, 9], ['bytes=1000-', 1000, f.bytes.length - 1], ['bytes=-9', f.bytes.length - 9, f.bytes.length - 1], ['bytes=2-999999', 2, f.bytes.length - 1]]) {
|
|
60
|
-
const r = await fetch(f.base + v.mediaUrl, { headers: { Range: range } });
|
|
61
|
-
assert.equal(r.status, 206);
|
|
62
|
-
assert.equal(r.headers.get('content-range'), `bytes ${start}-${end}/${f.bytes.length}`);
|
|
63
|
-
assert.deepEqual(Buffer.from(await r.arrayBuffer()), f.bytes.subarray(start, end + 1));
|
|
64
|
-
}
|
|
65
|
-
for (const range of ['bytes=99999-', 'bytes=4-2', 'bytes=-0', 'bytes=-', 'bytes=x-y']) {
|
|
66
|
-
const r = await fetch(f.base + v.mediaUrl, { headers: { Range: range } });
|
|
67
|
-
assert.equal(r.status, 416); assert.equal(r.headers.get('content-range'), `bytes */${f.bytes.length}`); await r.text();
|
|
68
|
-
}
|
|
69
|
-
for (const headers of [{ Range: 'bytes=0-1,4-5' }, { Range: 'bytes=0-2', 'If-Range': '"stale"' }]) {
|
|
70
|
-
const r = await fetch(f.base + v.mediaUrl, { headers }); assert.equal(r.status, 200); await r.arrayBuffer();
|
|
71
|
-
}
|
|
72
|
-
const full = await fetch(f.base + v.mediaUrl); const etag = full.headers.get('etag'); await full.arrayBuffer();
|
|
73
|
-
const r = await fetch(f.base + v.mediaUrl, { headers: { Range: 'bytes=0-1', 'If-Range': etag } });
|
|
74
|
-
assert.equal(r.status, 206); await r.arrayBuffer();
|
|
75
|
-
assert.equal(parseRange('bytes=0-0', 0), false);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('zero-copy original-path mapping survives restart and a fresh Node process without TTL', async (t) => {
|
|
79
|
-
const f = await setup(t), v = (await expose(f.plugin, [f.source])).output.videos[0];
|
|
80
|
-
const recordDir = path.join(f.context.appDataDir, 'video-share', v.id);
|
|
81
|
-
assert.deepEqual(await fs.readdir(recordDir), ['entry.json']);
|
|
82
|
-
const record = JSON.parse(await fs.readFile(path.join(recordDir, 'entry.json'), 'utf8'));
|
|
83
|
-
assert.equal(record.absolutePath, f.source);
|
|
84
|
-
f.restart();
|
|
85
|
-
const r = await fetch(f.base + v.mediaUrl); assert.equal(r.status, 200); assert.deepEqual(Buffer.from(await r.arrayBuffer()), f.bytes);
|
|
86
|
-
const script = `import { VideoStore } from ${JSON.stringify(new URL('./store.mjs', import.meta.url).href)}; Date.now = () => 9999999999999; const e = await new VideoStore(${JSON.stringify(path.join(f.context.appDataDir, 'video-share'))}).get(${JSON.stringify(v.id)}); if (!e || 'expiresAt' in e) process.exit(1); console.log(e.id);`;
|
|
87
|
-
const child = spawnSync(process.execPath, ['--input-type=module', '-e', script], { encoding: 'utf8' });
|
|
88
|
-
assert.equal(child.status, 0, child.stderr); assert.match(child.stdout, new RegExp(v.id));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('revoke persists across restarts; guessing, traversal, writes and missing blobs fail closed', async (t) => {
|
|
92
|
-
const f = await setup(t), v = (await expose(f.plugin, [f.source])).output.videos[0];
|
|
93
|
-
for (const suffix of ['f'.repeat(48), '%2e%2e%2fsecret', v.id + '/media/extra']) {
|
|
94
|
-
const r = await fetch(f.base + '/api/video-share/' + suffix); assert.equal(r.status, 404); await r.text();
|
|
95
|
-
}
|
|
96
|
-
const post = await fetch(f.base + v.url, { method: 'POST' }); assert.equal(post.status, 405); await post.text();
|
|
97
|
-
const revoke = f.plugin.tools.find((t) => t.name === 'revoke_videos');
|
|
98
|
-
assert.equal((await revoke.execute({ ids: [v.id] })).output.results[0].revoked, true);
|
|
99
|
-
assert.equal((await revoke.execute({ ids: [v.id] })).output.results[0].revoked, false);
|
|
100
|
-
f.restart(); const gone = await fetch(f.base + v.mediaUrl); assert.equal(gone.status, 404); await gone.text();
|
|
101
|
-
assert.equal((await fs.stat(f.source)).isFile(), true);
|
|
102
|
-
const next = (await expose(f.plugin, [f.source])).output.videos[0];
|
|
103
|
-
await fs.rm(f.source);
|
|
104
|
-
const missing = await fetch(f.base + next.mediaUrl); assert.equal(missing.status, 404); await missing.text();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('validation, partial batches, signatures, escaping and public origin', async (t) => {
|
|
108
|
-
const f = await setup(t), tool = f.plugin.tools[0];
|
|
109
|
-
for (const paths of [[], ['relative.mp4'], [12], Array(21).fill(f.source)]) assert.throws(() => tool.validate({ paths }));
|
|
110
|
-
const fake = path.join(f.root, 'fake.mp4'); await fs.writeFile(fake, '<script>not video</script>');
|
|
111
|
-
const result = await expose(f.plugin, [f.source, fake, path.join(f.root, 'a.txt')]);
|
|
112
|
-
assert.equal(result.ok, false); assert.equal(result.output.videos.length, 1); assert.equal(result.output.errors.length, 2);
|
|
113
|
-
const html = playerPage({ filename: '<script>alert(1)</script>', id: 'a'.repeat(48) });
|
|
114
|
-
assert.ok(!html.includes('<script>')); assert.match(html, /<script>/);
|
|
115
|
-
const publicPlugin = createPlugin({ ...f.context, env: { NEO_VIDEO_SHARE_PUBLIC_ORIGIN: 'https://videos.example.com' } });
|
|
116
|
-
const v = (await expose(publicPlugin, [f.source])).output.videos[0];
|
|
117
|
-
assert.ok(v.url.startsWith('https://videos.example.com/api/video-share/')); assert.equal(v.reference, undefined);
|
|
118
|
-
assert.ok((await expose(publicPlugin, [f.source])).output._ui.resources[0].url.startsWith('/api/video-share/'));
|
|
119
|
-
assert.throws(() => createPlugin({ env: { NEO_VIDEO_SHARE_PUBLIC_ORIGIN: 'https://host/path' } }));
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('concurrent publishers use independent atomic records', async (t) => {
|
|
123
|
-
const f = await setup(t);
|
|
124
|
-
const store = new VideoStore(path.join(f.context.appDataDir, 'video-share'));
|
|
125
|
-
const entries = await Promise.all(Array.from({ length: 8 }, () => store.publish(f.source)));
|
|
126
|
-
assert.equal(new Set(entries.map((e) => e.id)).size, 8);
|
|
127
|
-
for (const e of entries) assert.equal((await store.get(e.id)).id, e.id);
|
|
128
|
-
assert.ok((await fs.readdir(store.directory)).every((s) => !s.startsWith('.pending')));
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('source move invalidates player and media after restart, no fallback copy', async (t) => {
|
|
132
|
-
const f = await setup(t), v = (await expose(f.plugin, [f.source])).output.videos[0];
|
|
133
|
-
await fs.rename(f.source, path.join(f.root, 'moved.mp4'));
|
|
134
|
-
f.restart();
|
|
135
|
-
for (const url of [v.url, v.mediaUrl]) {
|
|
136
|
-
const r = await fetch(f.base + url); assert.equal(r.status, 404); await r.text();
|
|
137
|
-
}
|
|
138
|
-
assert.deepEqual(await fs.readdir(path.join(f.context.appDataDir, 'video-share', v.id)), ['entry.json']);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test('same-path updates serve current bytes and invalidate old If-Range validators', async (t) => {
|
|
142
|
-
const f = await setup(t), v = (await expose(f.plugin, [f.source])).output.videos[0];
|
|
143
|
-
const before = await fetch(f.base + v.mediaUrl); const oldTag = before.headers.get('etag'); await before.arrayBuffer();
|
|
144
|
-
const updated = Buffer.from(f.bytes); updated[100] = 42;
|
|
145
|
-
await fs.writeFile(f.source, updated);
|
|
146
|
-
await fs.utimes(f.source, new Date(), new Date(Date.now() + 5000));
|
|
147
|
-
const r = await fetch(f.base + v.mediaUrl, { headers: { Range: 'bytes=0-9', 'If-Range': oldTag } });
|
|
148
|
-
assert.equal(r.status, 200); assert.notEqual(r.headers.get('etag'), oldTag);
|
|
149
|
-
assert.deepEqual(Buffer.from(await r.arrayBuffer()), updated);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test('v1 copied publications are not silently used as zero-copy sources', async (t) => {
|
|
153
|
-
const f = await setup(t), id = 'a'.repeat(48);
|
|
154
|
-
const dir = path.join(f.context.appDataDir, 'video-share', id);
|
|
155
|
-
await fs.mkdir(dir, { recursive: true });
|
|
156
|
-
await fs.writeFile(path.join(dir, 'entry.json'), JSON.stringify({ version: 1, id, filename: 'old.mp4', contentType: 'video/mp4' }));
|
|
157
|
-
await fs.writeFile(path.join(dir, 'media'), f.bytes);
|
|
158
|
-
const r = await fetch(f.base + '/api/video-share/' + id + '/media'); assert.equal(r.status, 404); await r.text();
|
|
159
|
-
assert.deepEqual(await fs.readFile(path.join(dir, 'media')), f.bytes); // no automatic user-data deletion
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test('presenter embeds valid videos including partial failures and historical results, no text links', async (t) => {
|
|
163
|
-
const f = await setup(t);
|
|
164
|
-
const result = await expose(f.plugin, [f.source, path.join(f.root, 'missing.mp4')]);
|
|
165
|
-
assert.equal(result.ok, false);
|
|
166
|
-
const shown = f.plugin.presentToolResult({ toolName: 'expose_videos', output: result.output, ok: false });
|
|
167
|
-
assert.equal(shown.presentationLevel, 'primary');
|
|
168
|
-
assert.equal(shown.resources.length, 1);
|
|
169
|
-
assert.equal(shown.resources[0].kind, 'embed');
|
|
170
|
-
assert.match(shown.resources[0].url, /\?embed=1$/);
|
|
171
|
-
assert.ok(!shown.text.includes('/api/'));
|
|
172
|
-
assert.match(result.output.usage, /Do not repeat video URLs/);
|
|
173
|
-
const old = { videos: [{ ...result.output.videos[0], url: 'https://untrusted.example/', markdown: '[old](https://untrusted.example/)' }] };
|
|
174
|
-
assert.ok(f.plugin.presentToolResult({ toolName: 'expose_videos', output: old, ok: true }).resources[0].url.startsWith('/api/video-share/'));
|
|
175
|
-
assert.equal(f.plugin.presentToolResult({ toolName: 'revoke_videos', output: result.output }), undefined);
|
|
176
|
-
assert.equal(f.plugin.presentToolResult({ toolName: 'expose_videos', output: { videos: [{ id: '../escape' }] } }), undefined);
|
|
177
|
-
const page = await fetch(f.base + shown.resources[0].url + '&theme=light');
|
|
178
|
-
const html = await page.text();
|
|
179
|
-
assert.equal(page.status, 200); assert.match(html, /data-theme="light"/);
|
|
180
|
-
assert.ok(!/<a\b|target=|window\.open/.test(html));
|
|
181
|
-
assert.match(html, /<video controls playsinline/);
|
|
182
|
-
});
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { Worker } from 'node:worker_threads';
|
|
7
|
-
import { execFileSync } from 'node:child_process';
|
|
8
|
-
import { renderXhsEditorPage } from './editor-page.mjs';
|
|
9
|
-
import { XhsArtifactRegistry, createOpenXhsArtifactEditorTool, createReadXhsArtifactTool, serveXhsArtifact } from './artifacts.mjs';
|
|
10
|
-
const payload = { title: 'title', body: 'body', interaction: '', hashtags: [], images: [{ url: '', caption: 'planned', overlay: '', note: '' }], review: '' };
|
|
11
|
-
function fixture(t) { const storageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xhs-version-')); t.after(() => fs.rmSync(storageDir, { recursive: true, force: true })); return new XhsArtifactRegistry({ storageDir }); }
|
|
12
|
-
test('CAS rejects missing/stale versions, independent registries read disk, failed persistence does not publish', t => {
|
|
13
|
-
const a = fixture(t), item = a.add({ payload, sessionId: 's' });
|
|
14
|
-
const b = new XhsArtifactRegistry({ storageDir: a.storageDir }); b.get(item.id, 's');
|
|
15
|
-
assert.throws(() => a.update(item.id, { payload }, 's'), { code: 'version_conflict' });
|
|
16
|
-
a.update(item.id, { payload: { ...payload, body: 'saved' } }, 's', 1);
|
|
17
|
-
assert.equal(b.get(item.id, 's').version, 2);
|
|
18
|
-
assert.throws(() => b.update(item.id, { payload }, 's', 1), { code: 'version_conflict', current_version: 2 });
|
|
19
|
-
a.persist = () => { throw new Error('disk full'); };
|
|
20
|
-
assert.throws(() => a.update(item.id, { payload }, 's', 2), /disk full/);
|
|
21
|
-
assert.equal(a.get(item.id, 's').payload.body, 'saved');
|
|
22
|
-
assert.equal(fs.existsSync(a.artifactFile(item.id) + '.lock'), false);
|
|
23
|
-
assert.equal(a.get(item.id), undefined); assert.equal(a.get(item.id, 'other'), undefined);
|
|
24
|
-
});
|
|
25
|
-
test('parallel workers never both win same version', async t => {
|
|
26
|
-
for (let round = 0; round < Number(process.env.XHS_CAS_ROUNDS || 1); round++) {
|
|
27
|
-
const registry = fixture(t), item = registry.add({ payload, sessionId: 's' });
|
|
28
|
-
const moduleUrl = new URL('./artifacts.mjs', import.meta.url).href;
|
|
29
|
-
const run = body => new Promise((resolve, reject) => {
|
|
30
|
-
const worker = new Worker(`const {parentPort,workerData}=require('node:worker_threads'); import(workerData.moduleUrl).then(({XhsArtifactRegistry})=>{const r=new XhsArtifactRegistry({storageDir:workerData.dir});try{const a=r.update(workerData.id,{payload:workerData.payload},'s',1);parentPort.postMessage({version:a.version,body:a.payload.body})}catch(e){parentPort.postMessage({code:e.code,message:e.message,syscall:e.syscall,path:e.path,stack:e.stack})}})`, { eval: true, workerData: { moduleUrl, dir: registry.storageDir, id: item.id, payload: { ...payload, body } } }); worker.on('message', resolve); worker.on('error', reject);
|
|
31
|
-
});
|
|
32
|
-
const results = await Promise.all([run('one'), run('two'), run('three'), run('four')]);
|
|
33
|
-
const diagnostic = JSON.stringify({ round, results }, null, 2);
|
|
34
|
-
assert.equal(results.filter(r => r.version === 2).length, 1, diagnostic);
|
|
35
|
-
assert.equal(results.filter(r => ['artifact_busy', 'version_conflict'].includes(r.code)).length, 3, diagnostic);
|
|
36
|
-
assert.equal(registry.get(item.id, 's').payload.body, results.find(r => r.version === 2).body, diagnostic);
|
|
37
|
-
assert.equal(registry.get(item.id, 's').version, 2);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
test('model cached read binds session/agent/id/version, preserves saved edits and rejects concurrent user save/replay/restart', async t => {
|
|
41
|
-
const registry = fixture(t), context = { session: { sessionId: 's' } };
|
|
42
|
-
const open = createOpenXhsArtifactEditorTool({ registry }), read = createReadXhsArtifactTool({ registry });
|
|
43
|
-
const item = registry.add({ payload, sessionId: 's' });
|
|
44
|
-
await assert.rejects(() => open.execute(open.validate({ artifact_id: item.id, payload }), context), { code: 'read_required' });
|
|
45
|
-
registry.update(item.id, { payload: { ...payload, body: 'user edit' } }, 's', 1);
|
|
46
|
-
let latest = (await read.execute({ id: item.id }, context)).output;
|
|
47
|
-
const input = open.validate({ artifact_id: item.id, payload: { ...latest.artifact.payload, review: 'model review' } });
|
|
48
|
-
await assert.rejects(() => open.execute(input, { session: { sessionId: 'other' } }), { code: 'read_required' });
|
|
49
|
-
await assert.rejects(() => open.execute(input, { ...context, agentId: 'other-agent' }), { code: 'read_required' });
|
|
50
|
-
const other = registry.add({ payload, sessionId: 's' });
|
|
51
|
-
await assert.rejects(() => open.execute(open.validate({ artifact_id: other.id, payload }), context), { code: 'read_required' });
|
|
52
|
-
const updated = await open.execute(input, context); assert.equal(updated.output.artifact.payload.body, 'user edit');
|
|
53
|
-
await assert.rejects(() => open.execute(input, context), { code: 'read_required' });
|
|
54
|
-
latest = (await read.execute({ id: item.id }, context)).output;
|
|
55
|
-
registry.update(item.id, { payload: { ...payload, body: 'new user edit' } }, 's', 3);
|
|
56
|
-
await assert.rejects(() => open.execute(open.validate({ artifact_id: item.id, payload }), context), { code: 'version_conflict' });
|
|
57
|
-
assert.equal(registry.get(item.id, 's').payload.body, 'new user edit');
|
|
58
|
-
await read.execute({ id: item.id }, context);
|
|
59
|
-
registry.readVersions.get(registry.readKey(item.id, context)).expires = 0;
|
|
60
|
-
await assert.rejects(() => open.execute(input, context), { code: 'read_required' });
|
|
61
|
-
const restarted = new XhsArtifactRegistry({ storageDir: registry.storageDir });
|
|
62
|
-
assert.throws(() => restarted.updateFromRead(item.id, { payload }, context), { code: 'read_required' });
|
|
63
|
-
});
|
|
64
|
-
test('legacy version defaults to 1; HTTP conflict is explicit and isolated', async t => {
|
|
65
|
-
const registry = fixture(t), item = registry.add({ payload, sessionId: 's' });
|
|
66
|
-
const legacy = { ...item }; delete legacy.version; fs.writeFileSync(registry.artifactFile(item.id), JSON.stringify(legacy));
|
|
67
|
-
assert.equal(registry.get(item.id, 's').version, 1);
|
|
68
|
-
let status, body; const res = { writeHead(s) { status = s; }, end(text) { body = JSON.parse(text); } };
|
|
69
|
-
await serveXhsArtifact(registry, { method: 'PUT' }, res, item.id, async () => ({ title: payload.title, payload, expected_version: 0 }), 's');
|
|
70
|
-
assert.equal(status, 409); assert.equal(body.code, 'version_conflict'); assert.equal(body.current_version, 1);
|
|
71
|
-
await serveXhsArtifact(registry, { method: 'GET' }, res, item.id, null, 'other'); assert.equal(status, 404);
|
|
72
|
-
fs.writeFileSync(registry.artifactFile(item.id) + '.lock', 'crashed owner');
|
|
73
|
-
await serveXhsArtifact(registry, { method: 'PUT' }, res, item.id, async () => ({ title: payload.title, payload, expected_version: 1 }), 's');
|
|
74
|
-
assert.equal(status, 423); assert.equal(body.code, 'artifact_busy');
|
|
75
|
-
fs.unlinkSync(registry.artifactFile(item.id) + '.lock');
|
|
76
|
-
registry.update(item.id, { payload }, 's', 1); assert.equal(registry.get(item.id, 's').version, 2);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('editor existing CSS is byte-equivalent to baseline', () => {
|
|
80
|
-
const baseline = execFileSync('git', ['show', 'HEAD:web/plugins/xhs-artifact/editor-page.mjs'], { encoding: 'utf8' });
|
|
81
|
-
const current = renderXhsEditorPage({ id: 'style', payload }, '/api/xhs-artifacts/style');
|
|
82
|
-
assert.equal(current.match(/<style>([\s\S]*?)<\/style>/)[1], baseline.match(/<style>([\s\S]*?)<\/style>/)[1]);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('Windows replacement retries hold CAS lock; exhausted writes are 500 and preserve old JSON', async t => {
|
|
86
|
-
const registry = fixture(t), item = registry.add({ payload, sessionId: 's' });
|
|
87
|
-
const second = new XhsArtifactRegistry({ storageDir: registry.storageDir });
|
|
88
|
-
const originalRename = fs.renameSync;
|
|
89
|
-
let calls = 0;
|
|
90
|
-
try {
|
|
91
|
-
fs.renameSync = (from, to) => {
|
|
92
|
-
calls++;
|
|
93
|
-
assert.equal(registry.get(item.id, 's').version, 1);
|
|
94
|
-
assert.throws(() => second.update(item.id, { payload }, 's', 1), { code: 'artifact_busy' });
|
|
95
|
-
if (calls < 3) throw Object.assign(new Error('injected Windows sharing denial'), { code: 'EPERM' });
|
|
96
|
-
return originalRename(from, to);
|
|
97
|
-
};
|
|
98
|
-
if (process.platform === 'win32') {
|
|
99
|
-
registry.update(item.id, { payload: { ...payload, body: 'committed' } }, 's', 1);
|
|
100
|
-
assert.equal(calls, 3);
|
|
101
|
-
assert.equal(registry.get(item.id, 's').payload.body, 'committed');
|
|
102
|
-
}
|
|
103
|
-
} finally { fs.renameSync = originalRename; }
|
|
104
|
-
const before = registry.get(item.id, 's');
|
|
105
|
-
let status, response;
|
|
106
|
-
const res = { writeHead(value) { status = value; }, end(value) { response = JSON.parse(value); } };
|
|
107
|
-
for (const code of ['EPERM', 'ENOSPC']) {
|
|
108
|
-
calls = 0;
|
|
109
|
-
try {
|
|
110
|
-
fs.renameSync = () => { calls++; throw Object.assign(new Error('injected persistent failure'), { code }); };
|
|
111
|
-
await serveXhsArtifact(registry, { method: 'PUT' }, res, item.id, async () => ({ title: payload.title, payload, expected_version: before.version }), 's');
|
|
112
|
-
} finally { fs.renameSync = originalRename; }
|
|
113
|
-
assert.equal(status, 500);
|
|
114
|
-
assert.equal(response.code, 'artifact_write_failed');
|
|
115
|
-
assert.equal(response.storage_code, code);
|
|
116
|
-
assert.equal(calls, process.platform === 'win32' && code === 'EPERM' ? 21 : 1);
|
|
117
|
-
assert.deepEqual(registry.get(item.id, 's'), before);
|
|
118
|
-
assert.deepEqual(fs.readdirSync(registry.storageDir), [item.id + '.json']);
|
|
119
|
-
}
|
|
120
|
-
registry.update(item.id, { payload }, 's', before.version);
|
|
121
|
-
assert.equal(registry.get(item.id, 's').version, before.version + 1);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('Windows lock open retries remain exclusive and persistent lock denial is not a conflict', async t => {
|
|
125
|
-
const registry = fixture(t), item = registry.add({ payload, sessionId: 's' });
|
|
126
|
-
const originalOpen = fs.openSync;
|
|
127
|
-
let calls = 0;
|
|
128
|
-
try {
|
|
129
|
-
fs.openSync = (name, flags, ...args) => {
|
|
130
|
-
if (flags === 'wx' && String(name).endsWith('.lock') && ++calls < 3) throw Object.assign(new Error('delete-pending lock'), { code: 'EPERM' });
|
|
131
|
-
return originalOpen(name, flags, ...args);
|
|
132
|
-
};
|
|
133
|
-
if (process.platform === 'win32') {
|
|
134
|
-
registry.update(item.id, { payload }, 's', 1);
|
|
135
|
-
assert.equal(calls, 3);
|
|
136
|
-
}
|
|
137
|
-
} finally { fs.openSync = originalOpen; }
|
|
138
|
-
const before = registry.get(item.id, 's');
|
|
139
|
-
let status, response;
|
|
140
|
-
const res = { writeHead(value) { status = value; }, end(value) { response = JSON.parse(value); } };
|
|
141
|
-
try {
|
|
142
|
-
fs.openSync = (name, flags, ...args) => {
|
|
143
|
-
if (flags === 'wx' && String(name).endsWith('.lock')) throw Object.assign(new Error('persistent permission denial'), { code: 'EACCES' });
|
|
144
|
-
return originalOpen(name, flags, ...args);
|
|
145
|
-
};
|
|
146
|
-
await serveXhsArtifact(registry, { method: 'PUT' }, res, item.id, async () => ({ title: payload.title, payload, expected_version: before.version }), 's');
|
|
147
|
-
} finally { fs.openSync = originalOpen; }
|
|
148
|
-
assert.equal(status, 500); assert.equal(response.code, 'artifact_write_failed'); assert.equal(response.storage_code, 'EACCES');
|
|
149
|
-
assert.deepEqual(registry.get(item.id, 's'), before);
|
|
150
|
-
assert.deepEqual(fs.readdirSync(registry.storageDir), [item.id + '.json']);
|
|
151
|
-
});
|