dsh-lan-peers 0.7.4 → 0.8.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.
@@ -0,0 +1,134 @@
1
+ // dsh-lan-peers · v0.8.0-β(DESIGN §18.2)CLI:group archives / group archive-delete。
2
+ //
3
+ // 两个子命令都只打本机回环的 hub 管理接口(/api/lan-peers/group/archives),
4
+ // 所以这里起一个只实现这两条路由的 node:http 桩,真跑 cli.mjs 子进程,
5
+ // 断言请求形状、stdout 文案与退出码(403/409 归「现在做不了」,其余算失败)。
6
+ import test from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import http from 'node:http';
9
+ import { execFile } from 'node:child_process';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { makeTempStateDir } from '../host/test/helpers.mjs';
14
+
15
+ const CLI = path.join(path.dirname(fileURLToPath(import.meta.url)), 'cli.mjs');
16
+ const MB = 1024 * 1024;
17
+
18
+ function runAsync(args) {
19
+ return new Promise((resolve) => {
20
+ execFile(process.execPath, [CLI, ...args], { encoding: 'utf8' }, (error, stdout, stderr) => {
21
+ resolve({ status: error ? error.code : 0, stdout, stderr });
22
+ });
23
+ });
24
+ }
25
+
26
+ /** 起一个只实现 group/archives 的管理接口桩;handler 决定状态码与响应体。 */
27
+ async function startAdmin(t, handler) {
28
+ const seen = [];
29
+ const server = http.createServer((req, res) => {
30
+ let raw = '';
31
+ req.on('data', (chunk) => { raw += chunk; });
32
+ req.on('end', () => {
33
+ const body = raw === '' ? null : JSON.parse(raw);
34
+ const call = { method: req.method, url: req.url, body };
35
+ seen.push(call);
36
+ const reply = handler(call) ?? {};
37
+ res.writeHead(reply.status ?? 200, { 'Content-Type': 'application/json' });
38
+ res.end(JSON.stringify(reply.body ?? {}));
39
+ });
40
+ });
41
+ const port = await new Promise((resolve) => {
42
+ server.listen(0, '127.0.0.1', () => resolve(server.address().port));
43
+ });
44
+ t.after(() => new Promise((resolve) => { server.close(() => resolve()); }));
45
+ const dir = makeTempStateDir('dsh-lan-peers-cli-archives-');
46
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
47
+ return { seen, url: 'http://127.0.0.1:' + port, dir };
48
+ }
49
+
50
+ const ARCHIVES = [
51
+ { file: '101-400.json', fromSeq: 101, toSeq: 400, count: 300, bytes: 12 * MB, createdAt: 1_700_000_000_000 },
52
+ { file: '1-100.json', fromSeq: 1, toSeq: 100, count: 100, bytes: 9 * MB, createdAt: 1_600_000_000_000 },
53
+ ];
54
+
55
+ test('group archives:逐行打印文件名 / seq 区间 / 条数 / 体积 / 时间,末行汇总', async (t) => {
56
+ const admin = await startAdmin(t, () => ({ body: { archives: ARCHIVES, totalBytes: 21 * MB } }));
57
+ const out = await runAsync(['group', 'archives', '--url', admin.url, '--state-dir', admin.dir]);
58
+ assert.equal(out.status, 0, out.stderr);
59
+ const lines = out.stdout.trim().split('\n');
60
+ assert.equal(lines.length, 2);
61
+ assert.match(lines[0], /^101-400\.json\tseq 101-400\t300 条\t12582912B\t1700000000000$/);
62
+ assert.match(lines[1], /^1-100\.json\tseq 1-100\t100 条\t9437184B\t1600000000000$/);
63
+ assert.match(out.stderr, /共 2 个归档 · 22020096 字节/);
64
+
65
+ assert.equal(admin.seen.length, 1);
66
+ assert.equal(admin.seen[0].method, 'GET');
67
+ assert.equal(admin.seen[0].url, '/api/lan-peers/group/archives');
68
+ });
69
+
70
+ test('group archives:没有归档时给一行说明,不打印空表头', async (t) => {
71
+ const admin = await startAdmin(t, () => ({ body: { archives: [], totalBytes: 0 } }));
72
+ const out = await runAsync(['group', 'archives', '--url', admin.url, '--state-dir', admin.dir]);
73
+ assert.equal(out.status, 0, out.stderr);
74
+ assert.equal(out.stdout.trim(), '没有归档(hub 侧还没有归档文件)');
75
+ assert.equal(out.stderr, '');
76
+ });
77
+
78
+ test('group archives:非 hub(403)按「现在做不了」退出 2,未启用(409)同理', async (t) => {
79
+ const forbidden = await startAdmin(t, () => ({ status: 403, body: { error: 'hub-only' } }));
80
+ const out403 = await runAsync(['group', 'archives', '--url', forbidden.url, '--state-dir', forbidden.dir]);
81
+ assert.equal(out403.status, 2);
82
+ assert.match(out403.stderr, /归档操作失败: 403 hub-only/);
83
+
84
+ const disabled = await startAdmin(t, () => ({ status: 409, body: { error: 'group-disabled' } }));
85
+ const out409 = await runAsync(['group', 'archives', '--url', disabled.url, '--state-dir', disabled.dir]);
86
+ assert.equal(out409.status, 2);
87
+ assert.match(out409.stderr, /409 group-disabled/);
88
+ });
89
+
90
+ test('group archives:门面未接线(501)如实报错并退出 1', async (t) => {
91
+ const admin = await startAdmin(t, () => ({ status: 501, body: { error: 'archives-unsupported' } }));
92
+ const out = await runAsync(['group', 'archives', '--url', admin.url, '--state-dir', admin.dir]);
93
+ assert.equal(out.status, 1);
94
+ assert.match(out.stderr, /归档操作失败: 501 archives-unsupported/);
95
+ assert.equal(out.stdout, '');
96
+ });
97
+
98
+ test('group archive-delete:POST { file, action:"delete" } 并打印释放的字节', async (t) => {
99
+ const admin = await startAdmin(t, () => ({ body: { ok: true, removedBytes: 12 * MB } }));
100
+ const out = await runAsync(['group', 'archive-delete', '101-400.json', '--url', admin.url, '--state-dir', admin.dir]);
101
+ assert.equal(out.status, 0, out.stderr);
102
+ assert.equal(out.stdout.trim(), '已删除归档 101-400.json,释放 12582912 字节');
103
+ assert.equal(admin.seen.length, 1);
104
+ assert.equal(admin.seen[0].method, 'POST');
105
+ assert.equal(admin.seen[0].url, '/api/lan-peers/group/archives');
106
+ assert.deepEqual(admin.seen[0].body, { file: '101-400.json', action: 'delete' });
107
+ });
108
+
109
+ test('group archive-delete:未知归档(404)退出 1,权限失败(403)退出 2', async (t) => {
110
+ const unknown = await startAdmin(t, () => ({ status: 404, body: { error: 'unknown-archive' } }));
111
+ const out404 = await runAsync(['group', 'archive-delete', 'nope.json', '--url', unknown.url, '--state-dir', unknown.dir]);
112
+ assert.equal(out404.status, 1);
113
+ assert.match(out404.stderr, /归档操作失败: 404 unknown-archive/);
114
+
115
+ const forbidden = await startAdmin(t, () => ({ status: 403, body: { error: 'hub-only' } }));
116
+ const out403 = await runAsync(['group', 'archive-delete', '1-100.json', '--url', forbidden.url, '--state-dir', forbidden.dir]);
117
+ assert.equal(out403.status, 2);
118
+ assert.match(out403.stderr, /403 hub-only/);
119
+ });
120
+
121
+ test('group archive-delete:缺文件名直接报用法,不发请求', async (t) => {
122
+ const admin = await startAdmin(t, () => ({ body: { ok: true, removedBytes: 0 } }));
123
+ const out = await runAsync(['group', 'archive-delete', '--url', admin.url, '--state-dir', admin.dir]);
124
+ assert.equal(out.status, 1);
125
+ assert.match(out.stderr, /用法: dsh-lan-peers group archive-delete <file>/);
126
+ assert.equal(admin.seen.length, 0);
127
+ });
128
+
129
+ test('group help:两个归档子命令都出现在帮助里', async () => {
130
+ const out = await runAsync(['group', 'help']);
131
+ assert.equal(out.status, 0);
132
+ assert.match(out.stdout, /archives\s+列出 hub 侧归档文件/);
133
+ assert.match(out.stdout, /archive-delete <file>\s+删除一个归档文件/);
134
+ });