@wenrwa/marketplace-cli 0.8.11 → 0.8.13
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/dist/__tests__/bridge-cleanup-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-cleanup-frame.test.js +217 -0
- package/dist/__tests__/bridge-clone-grow-quote.test.d.ts +1 -0
- package/dist/__tests__/bridge-clone-grow-quote.test.js +145 -0
- package/dist/__tests__/bridge-disk-pressure-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-disk-pressure-frame.test.js +179 -0
- package/dist/__tests__/bridge-preview-checkout.test.js +9 -0
- package/dist/__tests__/bridge-storage-cleanup-real-fs.test.d.ts +1 -0
- package/dist/__tests__/bridge-storage-cleanup-real-fs.test.js +372 -0
- package/dist/__tests__/git-token-refresh.test.d.ts +1 -0
- package/dist/__tests__/git-token-refresh.test.js +231 -0
- package/dist/bridge/diskPreflight.d.ts +4 -1
- package/dist/bridge/diskPreflight.js +5 -3
- package/dist/bridge/gitTokenBroker.d.ts +28 -0
- package/dist/bridge/gitTokenBroker.js +124 -0
- package/dist/bridge/previewCheckout.d.ts +11 -0
- package/dist/bridge/previewCheckout.js +18 -1
- package/dist/bridge/relayClient.d.ts +47 -0
- package/dist/bridge/relayClient.js +150 -2
- package/dist/bridge/repoCheckout.d.ts +3 -1
- package/dist/bridge/repoCheckout.js +6 -1
- package/dist/bridge/storageCleanup.d.ts +66 -0
- package/dist/bridge/storageCleanup.js +337 -0
- package/dist/bridge/storageQuotaFetch.js +12 -0
- package/dist/bridge/storageQuotaPreflight.d.ts +12 -0
- package/dist/commands/bridge.d.ts +10 -0
- package/dist/commands/bridge.js +59 -3
- package/dist/index.js +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
/**
|
|
37
|
+
* stc-05 — the relay client reports what Clean up can free (`cleanup-report`,
|
|
38
|
+
* on connect and on every `inventory-scan`) and runs it on `cleanup-run`,
|
|
39
|
+
* answering `cleanup-result` and then a fresh report. A box whose bridge was
|
|
40
|
+
* built without cleanup answers `unsupported` and touches nothing.
|
|
41
|
+
*
|
|
42
|
+
* Faked relay (same harness as bridge-reclaim-frame.test.ts), real temp fs.
|
|
43
|
+
*/
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const ws_1 = require("ws");
|
|
48
|
+
const relayClient_1 = require("../bridge/relayClient");
|
|
49
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
50
|
+
class FakePty {
|
|
51
|
+
opts;
|
|
52
|
+
constructor(opts) {
|
|
53
|
+
this.opts = opts;
|
|
54
|
+
}
|
|
55
|
+
write() { }
|
|
56
|
+
resize() { }
|
|
57
|
+
kill() { }
|
|
58
|
+
onData() { }
|
|
59
|
+
onExit() { }
|
|
60
|
+
}
|
|
61
|
+
const makeFactory = () => ({ spawn: (o) => new FakePty(o) });
|
|
62
|
+
function frameQueue(ws) {
|
|
63
|
+
const frames = [];
|
|
64
|
+
const waiters = [];
|
|
65
|
+
ws.on('message', (raw) => {
|
|
66
|
+
const f = JSON.parse(String(raw));
|
|
67
|
+
const i = waiters.findIndex((w) => w.t === f.t);
|
|
68
|
+
if (i >= 0)
|
|
69
|
+
waiters.splice(i, 1)[0].resolve(f);
|
|
70
|
+
else
|
|
71
|
+
frames.push(f);
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
/** The next frame of type `t` (earlier frames of other types are kept). */
|
|
75
|
+
nextOf: (t) => {
|
|
76
|
+
const i = frames.findIndex((f) => f.t === t);
|
|
77
|
+
if (i >= 0)
|
|
78
|
+
return Promise.resolve(frames.splice(i, 1)[0]);
|
|
79
|
+
return new Promise((resolve) => waiters.push({ t, resolve }));
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function startFakeRelay() {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const wss = new ws_1.WebSocketServer({ port: 0, host: '127.0.0.1' });
|
|
86
|
+
const queue = [];
|
|
87
|
+
const waiters = [];
|
|
88
|
+
wss.on('connection', (ws) => { const w = waiters.shift(); if (w)
|
|
89
|
+
w(ws);
|
|
90
|
+
else
|
|
91
|
+
queue.push(ws); });
|
|
92
|
+
wss.on('listening', () => {
|
|
93
|
+
const addr = wss.address();
|
|
94
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
95
|
+
resolve({ wss, port, nextConnection: () => (queue.length > 0 ? Promise.resolve(queue.shift()) : new Promise((r) => waiters.push(r))) });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function closeRelay(wss) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
for (const client of wss.clients)
|
|
102
|
+
client.terminate();
|
|
103
|
+
wss.close((err) => (err ? reject(err) : resolve()));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
let root;
|
|
107
|
+
let repos;
|
|
108
|
+
let cache;
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'stc05-frame-'));
|
|
111
|
+
repos = path.join(root, 'repos');
|
|
112
|
+
cache = path.join(root, 'home', '.npm', '_cacache');
|
|
113
|
+
fs.mkdirSync(repos);
|
|
114
|
+
fs.mkdirSync(cache, { recursive: true });
|
|
115
|
+
fs.writeFileSync(path.join(cache, 'blob'), Buffer.alloc(64 * 1024, 1));
|
|
116
|
+
});
|
|
117
|
+
afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
118
|
+
const noGit = async () => ({ stdout: '', code: 1 });
|
|
119
|
+
function start(port, extra = {}) {
|
|
120
|
+
return (0, relayClient_1.startRelayClient)({
|
|
121
|
+
relayUrl: `ws://127.0.0.1:${port}`,
|
|
122
|
+
relayToken: 'tok',
|
|
123
|
+
sessions: new sessionManager_1.SessionManager(makeFactory()),
|
|
124
|
+
reposBaseDir: repos,
|
|
125
|
+
enableStorageCleanup: true,
|
|
126
|
+
storageCleanupDeps: { cacheDirs: [cache], run: noGit },
|
|
127
|
+
resolveWorkspaceCwd: async () => null,
|
|
128
|
+
...extra,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
describe('relay client — storage cleanup (stc-05)', () => {
|
|
132
|
+
it('reports the reclaimable figure on connect, stamped with the cleanup version', async () => {
|
|
133
|
+
const relay = await startFakeRelay();
|
|
134
|
+
const handle = start(relay.port);
|
|
135
|
+
const ws = await relay.nextConnection();
|
|
136
|
+
const q = frameQueue(ws);
|
|
137
|
+
const report = await q.nextOf('cleanup-report');
|
|
138
|
+
expect(report.cleanupVersion).toBe(1);
|
|
139
|
+
expect(report.reclaimableBytes).toBeGreaterThanOrEqual(64 * 1024);
|
|
140
|
+
expect(report.byCategory.caches).toBe(report.reclaimableBytes);
|
|
141
|
+
handle.close();
|
|
142
|
+
await closeRelay(relay.wss);
|
|
143
|
+
});
|
|
144
|
+
it('runs Clean up on `cleanup-run`, answers what it freed, then reports nothing left', async () => {
|
|
145
|
+
const relay = await startFakeRelay();
|
|
146
|
+
const handle = start(relay.port);
|
|
147
|
+
const ws = await relay.nextConnection();
|
|
148
|
+
const q = frameQueue(ws);
|
|
149
|
+
const first = await q.nextOf('cleanup-report');
|
|
150
|
+
ws.send(JSON.stringify({ t: 'cleanup-run' }));
|
|
151
|
+
const result = await q.nextOf('cleanup-result');
|
|
152
|
+
expect(result).toMatchObject({ outcome: 'done', freedBytes: first.reclaimableBytes });
|
|
153
|
+
expect(fs.existsSync(cache)).toBe(false);
|
|
154
|
+
const after = await q.nextOf('cleanup-report');
|
|
155
|
+
expect(after.reclaimableBytes).toBe(0);
|
|
156
|
+
handle.close();
|
|
157
|
+
await closeRelay(relay.wss);
|
|
158
|
+
});
|
|
159
|
+
it('re-reports on `inventory-scan` (the storage view opening)', async () => {
|
|
160
|
+
const relay = await startFakeRelay();
|
|
161
|
+
const handle = start(relay.port);
|
|
162
|
+
const ws = await relay.nextConnection();
|
|
163
|
+
const q = frameQueue(ws);
|
|
164
|
+
await q.nextOf('cleanup-report');
|
|
165
|
+
fs.writeFileSync(path.join(cache, 'more'), Buffer.alloc(32 * 1024, 2));
|
|
166
|
+
ws.send(JSON.stringify({ t: 'inventory-scan' }));
|
|
167
|
+
const again = await q.nextOf('cleanup-report');
|
|
168
|
+
expect(again.reclaimableBytes).toBeGreaterThanOrEqual(96 * 1024);
|
|
169
|
+
handle.close();
|
|
170
|
+
await closeRelay(relay.wss);
|
|
171
|
+
});
|
|
172
|
+
it('wires attachment to the live sessions: an open session keeps its checkout’s leftovers', async () => {
|
|
173
|
+
const leftover = path.join(repos, 'ws-1');
|
|
174
|
+
fs.mkdirSync(path.join(leftover, 'node_modules'), { recursive: true });
|
|
175
|
+
fs.writeFileSync(path.join(leftover, 'node_modules', 'x.js'), 'x');
|
|
176
|
+
const relay = await startFakeRelay();
|
|
177
|
+
const handle = start(relay.port);
|
|
178
|
+
const ws = await relay.nextConnection();
|
|
179
|
+
const q = frameQueue(ws);
|
|
180
|
+
await q.nextOf('cleanup-report');
|
|
181
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, sessionId: 'agent-1', workspaceId: 'ws-1' }));
|
|
182
|
+
await q.nextOf('opened');
|
|
183
|
+
ws.send(JSON.stringify({ t: 'cleanup-run' }));
|
|
184
|
+
await q.nextOf('cleanup-result');
|
|
185
|
+
expect(fs.existsSync(path.join(leftover, 'node_modules', 'x.js'))).toBe(true);
|
|
186
|
+
handle.close();
|
|
187
|
+
await closeRelay(relay.wss);
|
|
188
|
+
});
|
|
189
|
+
it('a bridge without cleanup answers `unsupported`, sends no report, and deletes nothing', async () => {
|
|
190
|
+
const relay = await startFakeRelay();
|
|
191
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
192
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
193
|
+
relayToken: 'tok',
|
|
194
|
+
sessions: new sessionManager_1.SessionManager(makeFactory()),
|
|
195
|
+
});
|
|
196
|
+
const ws = await relay.nextConnection();
|
|
197
|
+
const q = frameQueue(ws);
|
|
198
|
+
ws.send(JSON.stringify({ t: 'cleanup-run' }));
|
|
199
|
+
const result = await q.nextOf('cleanup-result');
|
|
200
|
+
expect(result.outcome).toBe('unsupported');
|
|
201
|
+
expect(fs.existsSync(path.join(cache, 'blob'))).toBe(true);
|
|
202
|
+
handle.close();
|
|
203
|
+
await closeRelay(relay.wss);
|
|
204
|
+
});
|
|
205
|
+
it('exposes runAutomaticCleanup for growth (stc-06), off unless the kill switch is on', async () => {
|
|
206
|
+
const relay = await startFakeRelay();
|
|
207
|
+
const off = start(relay.port);
|
|
208
|
+
expect(await off.runAutomaticCleanup()).toEqual({ ran: false, reason: 'disabled' });
|
|
209
|
+
expect(fs.existsSync(path.join(cache, 'blob'))).toBe(true);
|
|
210
|
+
off.close();
|
|
211
|
+
const on = start(relay.port, { storageCleanupDeps: { cacheDirs: [cache], run: noGit, autoCleanupEnabled: true } });
|
|
212
|
+
expect(await on.runAutomaticCleanup()).toMatchObject({ ran: true });
|
|
213
|
+
expect(fs.existsSync(cache)).toBe(false);
|
|
214
|
+
on.close();
|
|
215
|
+
await closeRelay(relay.wss);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* stc-04 (docs/prd/storage-billed-in-credits.md) — the clone preflight's
|
|
5
|
+
* growth QUOTE travels from the backend's 409 to the pane's error frame
|
|
6
|
+
* unchanged, so the arena can ask "Grow / Cancel". The bridge never calls
|
|
7
|
+
* the grow route itself: it spends money and is deliberately not on the
|
|
8
|
+
* connect-token allowlist — the arena's Grow click does it with the user's
|
|
9
|
+
* session. No quote (old backend, flag off) → the frame is byte-identical
|
|
10
|
+
* to the old refusal.
|
|
11
|
+
*/
|
|
12
|
+
const storageQuotaFetch_1 = require("../bridge/storageQuotaFetch");
|
|
13
|
+
const repoCheckout_1 = require("../bridge/repoCheckout");
|
|
14
|
+
const diskPreflight_1 = require("../bridge/diskPreflight");
|
|
15
|
+
const storageQuotaPreflight_1 = require("../bridge/storageQuotaPreflight");
|
|
16
|
+
const relayClient_1 = require("../bridge/relayClient");
|
|
17
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
18
|
+
const QUOTE = { addGb: 5, newDiskGb: 15, newCreditsPerDay: 50 };
|
|
19
|
+
const REFUSAL_BODY = {
|
|
20
|
+
code: 'STORAGE_QUOTA_EXCEEDED',
|
|
21
|
+
tier: 'payg',
|
|
22
|
+
capGb: 10,
|
|
23
|
+
usedGb: 9.2,
|
|
24
|
+
additionalGb: 3.1,
|
|
25
|
+
suggestedTier: 'pro',
|
|
26
|
+
suggestedCapGb: 25,
|
|
27
|
+
tierName: 'Pay-as-you-go',
|
|
28
|
+
suggestedTierName: 'Pro',
|
|
29
|
+
};
|
|
30
|
+
function fetch409(error) {
|
|
31
|
+
return async () => ({ ok: false, status: 409, json: async () => ({ success: false, error }) });
|
|
32
|
+
}
|
|
33
|
+
describe('fetchStorageQuotaFact — the growth quote (stc-04)', () => {
|
|
34
|
+
const base = { apiUrl: 'https://api.test', connectToken: 'tok', usedGb: 9.2, additionalGb: 3.1 };
|
|
35
|
+
it('a 409 with a quote resolves a blocked fact carrying the quote unchanged', async () => {
|
|
36
|
+
const fact = await (0, storageQuotaFetch_1.fetchStorageQuotaFact)({ ...base, fetchImpl: fetch409({ ...REFUSAL_BODY, quote: QUOTE }) });
|
|
37
|
+
expect(fact?.allowed).toBe(false);
|
|
38
|
+
expect(fact?.quote).toEqual(QUOTE);
|
|
39
|
+
});
|
|
40
|
+
it('a 409 without a quote (old backend, billing off) carries no quote key at all', async () => {
|
|
41
|
+
const fact = await (0, storageQuotaFetch_1.fetchStorageQuotaFact)({ ...base, fetchImpl: fetch409(REFUSAL_BODY) });
|
|
42
|
+
expect(fact?.allowed).toBe(false);
|
|
43
|
+
expect(fact && 'quote' in fact).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
it('a malformed quote is dropped, never half-passed', async () => {
|
|
46
|
+
for (const quote of [{ addGb: 5 }, { addGb: '5', newDiskGb: 15, newCreditsPerDay: 50 }, null, 'x', { addGb: 0, newDiskGb: 15, newCreditsPerDay: 50 }]) {
|
|
47
|
+
const fact = await (0, storageQuotaFetch_1.fetchStorageQuotaFact)({ ...base, fetchImpl: fetch409({ ...REFUSAL_BODY, quote }) });
|
|
48
|
+
expect(fact?.allowed).toBe(false);
|
|
49
|
+
expect(fact && 'quote' in fact).toBe(false);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
const OVER = {
|
|
54
|
+
allowed: false,
|
|
55
|
+
tier: 'payg',
|
|
56
|
+
capGb: 10,
|
|
57
|
+
usedGb: 9.2,
|
|
58
|
+
additionalGb: 3.1,
|
|
59
|
+
suggestedTier: 'pro',
|
|
60
|
+
suggestedCapGb: 25,
|
|
61
|
+
tierName: 'Pay-as-you-go',
|
|
62
|
+
suggestedTierName: 'Pro',
|
|
63
|
+
};
|
|
64
|
+
function repoDeps(fact) {
|
|
65
|
+
const runs = [];
|
|
66
|
+
return {
|
|
67
|
+
runs,
|
|
68
|
+
deps: {
|
|
69
|
+
reposBaseDir: '/data/repos',
|
|
70
|
+
exists: () => false,
|
|
71
|
+
mkdir: () => { },
|
|
72
|
+
run: async (cmd, args) => { runs.push([cmd, ...args]); return { stdout: '', code: 0 }; },
|
|
73
|
+
freeMb: () => 999_999,
|
|
74
|
+
estimateSizeMb: () => 3200,
|
|
75
|
+
checkStorageQuota: async () => fact,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
describe('ensureWorkspaceRepo — a quoted refusal (stc-04)', () => {
|
|
80
|
+
it('refuses with the quota code and passes the quote through; git never runs', async () => {
|
|
81
|
+
const { deps, runs } = repoDeps({ ...OVER, quote: QUOTE });
|
|
82
|
+
const result = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-q', repoUrl: 'https://github.com/acme/w.git' }, deps);
|
|
83
|
+
expect(result.action).toBe('refused');
|
|
84
|
+
expect(result.code).toBe(storageQuotaPreflight_1.CLONE_QUOTA_PREFLIGHT_REFUSED);
|
|
85
|
+
expect(result.quote).toEqual(QUOTE);
|
|
86
|
+
expect(runs).toHaveLength(0);
|
|
87
|
+
});
|
|
88
|
+
it('no quote → the old refusal exactly, with no quote key', async () => {
|
|
89
|
+
const { deps } = repoDeps(OVER);
|
|
90
|
+
const result = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-q', repoUrl: 'https://github.com/acme/w.git' }, deps);
|
|
91
|
+
expect(result.action).toBe('refused');
|
|
92
|
+
expect(result.code).toBe(storageQuotaPreflight_1.CLONE_QUOTA_PREFLIGHT_REFUSED);
|
|
93
|
+
expect('quote' in result).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
class FakePty {
|
|
97
|
+
write() { }
|
|
98
|
+
resize() { }
|
|
99
|
+
kill() { }
|
|
100
|
+
onData() { }
|
|
101
|
+
onExit() { }
|
|
102
|
+
}
|
|
103
|
+
function fakeSocket() {
|
|
104
|
+
const handlers = new Map();
|
|
105
|
+
const sock = {
|
|
106
|
+
readyState: 1,
|
|
107
|
+
sent: [],
|
|
108
|
+
send(raw) { sock.sent.push(JSON.parse(raw)); },
|
|
109
|
+
close() { handlers.get('close')?.(1006); },
|
|
110
|
+
terminate() { handlers.get('close')?.(1006); },
|
|
111
|
+
on(event, cb) { handlers.set(event, cb); },
|
|
112
|
+
fire(event, ...a) { handlers.get(event)?.(...a); },
|
|
113
|
+
};
|
|
114
|
+
return sock;
|
|
115
|
+
}
|
|
116
|
+
async function refusalFrame(err) {
|
|
117
|
+
const socket = fakeSocket();
|
|
118
|
+
const spawned = [];
|
|
119
|
+
const factory = { spawn: () => { spawned.push('pty'); return new FakePty(); } };
|
|
120
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
121
|
+
relayUrl: 'ws://relay.test',
|
|
122
|
+
relayToken: 'tok',
|
|
123
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
124
|
+
wsFactory: () => socket,
|
|
125
|
+
resolveWorkspaceCwd: async () => { throw err; },
|
|
126
|
+
setTimeoutFn: () => 0,
|
|
127
|
+
clearTimeoutFn: () => { },
|
|
128
|
+
});
|
|
129
|
+
socket.fire('open');
|
|
130
|
+
socket.fire('message', JSON.stringify({ t: 'open', cols: 80, rows: 24, workspaceId: 'ws-1', repoUrl: 'https://github.com/acme/w', sessionId: 'agt-1' }));
|
|
131
|
+
await new Promise((r) => setImmediate(r));
|
|
132
|
+
handle.close();
|
|
133
|
+
return { frame: socket.sent.find((f) => f.t === 'error'), spawned };
|
|
134
|
+
}
|
|
135
|
+
describe('relay client — the quote reaches the pane on the error frame (stc-04)', () => {
|
|
136
|
+
it('a quoted refusal sends the quote unchanged beside code and message', async () => {
|
|
137
|
+
const { frame, spawned } = await refusalFrame(new diskPreflight_1.ClonePreflightRefusalError('Your plan…', storageQuotaPreflight_1.CLONE_QUOTA_PREFLIGHT_REFUSED, QUOTE));
|
|
138
|
+
expect(spawned).toHaveLength(0);
|
|
139
|
+
expect(frame).toEqual({ t: 'error', sessionId: 'agt-1', code: storageQuotaPreflight_1.CLONE_QUOTA_PREFLIGHT_REFUSED, message: 'Your plan…', quote: QUOTE });
|
|
140
|
+
});
|
|
141
|
+
it('no quote → the old frame exactly', async () => {
|
|
142
|
+
const { frame } = await refusalFrame(new diskPreflight_1.ClonePreflightRefusalError('Your plan…', storageQuotaPreflight_1.CLONE_QUOTA_PREFLIGHT_REFUSED));
|
|
143
|
+
expect(frame).toEqual({ t: 'error', sessionId: 'agt-1', code: storageQuotaPreflight_1.CLONE_QUOTA_PREFLIGHT_REFUSED, message: 'Your plan…' });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fs = __importStar(require("fs"));
|
|
37
|
+
const os = __importStar(require("os"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const relayClient_1 = require("../bridge/relayClient");
|
|
40
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
41
|
+
/**
|
|
42
|
+
* stc-06 — the bridge's half of automatic growth.
|
|
43
|
+
*
|
|
44
|
+
* - `disk: { freeGb, totalGb }` rides the periodic `sessions` frame when the
|
|
45
|
+
* embedder supplies `readDiskSpace` (the container lane). Its presence is
|
|
46
|
+
* the backend's capability flag; a null read (unknown) omits it.
|
|
47
|
+
* - `cleanup-auto` runs AUTOMATIC cleanup — behind the bridge's own kill
|
|
48
|
+
* switch (HIVEOP_AUTO_CLEANUP) — and answers `cleanup-auto-result`.
|
|
49
|
+
*/
|
|
50
|
+
class FakePty {
|
|
51
|
+
write() { }
|
|
52
|
+
resize() { }
|
|
53
|
+
kill() { }
|
|
54
|
+
onData() { }
|
|
55
|
+
onExit() { }
|
|
56
|
+
}
|
|
57
|
+
const factory = { spawn: () => new FakePty() };
|
|
58
|
+
function fakeSocket() {
|
|
59
|
+
const handlers = new Map();
|
|
60
|
+
const sock = {
|
|
61
|
+
readyState: 1,
|
|
62
|
+
sent: [],
|
|
63
|
+
send(raw) { sock.sent.push(JSON.parse(raw)); },
|
|
64
|
+
close() { handlers.get('close')?.(1006); },
|
|
65
|
+
terminate() { handlers.get('close')?.(1006); },
|
|
66
|
+
on(event, cb) { handlers.set(event, cb); },
|
|
67
|
+
fire(event, ...a) { handlers.get(event)?.(...a); },
|
|
68
|
+
};
|
|
69
|
+
return sock;
|
|
70
|
+
}
|
|
71
|
+
let root;
|
|
72
|
+
let repos;
|
|
73
|
+
let cache;
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'stc06-frame-'));
|
|
76
|
+
repos = path.join(root, 'repos');
|
|
77
|
+
cache = path.join(root, 'home', '.npm', '_cacache');
|
|
78
|
+
fs.mkdirSync(repos);
|
|
79
|
+
fs.mkdirSync(cache, { recursive: true });
|
|
80
|
+
fs.writeFileSync(path.join(cache, 'blob'), Buffer.alloc(64 * 1024, 1));
|
|
81
|
+
});
|
|
82
|
+
afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
83
|
+
const noGit = async () => ({ stdout: '', code: 1 });
|
|
84
|
+
function start(extra = {}) {
|
|
85
|
+
const socket = fakeSocket();
|
|
86
|
+
const intervals = [];
|
|
87
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
88
|
+
relayUrl: 'ws://relay.test',
|
|
89
|
+
relayToken: 'tok',
|
|
90
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
91
|
+
wsFactory: () => socket,
|
|
92
|
+
reportSessions: true,
|
|
93
|
+
setSessionsIntervalFn: (cb) => { intervals.push(cb); return intervals.length; },
|
|
94
|
+
clearSessionsIntervalFn: () => { },
|
|
95
|
+
setTimeoutFn: () => 0,
|
|
96
|
+
clearTimeoutFn: () => { },
|
|
97
|
+
...extra,
|
|
98
|
+
});
|
|
99
|
+
socket.fire('open');
|
|
100
|
+
const tick = () => { for (const cb of intervals)
|
|
101
|
+
cb(); };
|
|
102
|
+
const ofType = (t) => socket.sent.filter((f) => f.t === t);
|
|
103
|
+
const receive = (frame) => socket.fire('message', JSON.stringify(frame));
|
|
104
|
+
return { handle, socket, tick, ofType, receive };
|
|
105
|
+
}
|
|
106
|
+
const until = async (cond) => {
|
|
107
|
+
for (let i = 0; i < 200 && !cond(); i++)
|
|
108
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
109
|
+
};
|
|
110
|
+
describe('stc-06 — disk space on the sessions frame', () => {
|
|
111
|
+
it('carries { freeGb, totalGb } on connect and every tick, rounded to 0.01 GB', () => {
|
|
112
|
+
let free = 0.4567;
|
|
113
|
+
const b = start({ readDiskSpace: () => ({ freeGb: free, totalGb: 10.0001 }) });
|
|
114
|
+
expect(b.ofType('sessions')[0]).toEqual({ t: 'sessions', ids: [], disk: { freeGb: 0.46, totalGb: 10 } });
|
|
115
|
+
free = 3;
|
|
116
|
+
b.tick();
|
|
117
|
+
expect(b.ofType('sessions')[1].disk).toEqual({ freeGb: 3, totalGb: 10 });
|
|
118
|
+
b.handle.close();
|
|
119
|
+
});
|
|
120
|
+
it('an unknown read (null) or a throwing read omits the field — never a guess', () => {
|
|
121
|
+
const a = start({ readDiskSpace: () => null });
|
|
122
|
+
expect(a.ofType('sessions')[0]).toEqual({ t: 'sessions', ids: [] });
|
|
123
|
+
a.handle.close();
|
|
124
|
+
const b = start({ readDiskSpace: () => { throw new Error('EIO'); } });
|
|
125
|
+
expect(b.ofType('sessions')[0]).toEqual({ t: 'sessions', ids: [] });
|
|
126
|
+
b.handle.close();
|
|
127
|
+
});
|
|
128
|
+
it('without readDiskSpace (the laptop lane, old embedders) the frame is unchanged', () => {
|
|
129
|
+
const b = start();
|
|
130
|
+
expect(b.ofType('sessions')[0]).toEqual({ t: 'sessions', ids: [] });
|
|
131
|
+
b.handle.close();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe('stc-06 — cleanup-auto', () => {
|
|
135
|
+
it('kill switch off → answers ran:false and deletes nothing', async () => {
|
|
136
|
+
const b = start({ reposBaseDir: repos, enableStorageCleanup: true, storageCleanupDeps: { cacheDirs: [cache], run: noGit } });
|
|
137
|
+
b.receive({ t: 'cleanup-auto' });
|
|
138
|
+
await until(() => b.ofType('cleanup-auto-result').length > 0);
|
|
139
|
+
expect(b.ofType('cleanup-auto-result')).toEqual([{ t: 'cleanup-auto-result', ran: false, reason: 'disabled' }]);
|
|
140
|
+
expect(fs.existsSync(path.join(cache, 'blob'))).toBe(true);
|
|
141
|
+
b.handle.close();
|
|
142
|
+
});
|
|
143
|
+
it('kill switch on → runs automatic cleanup and answers what it freed', async () => {
|
|
144
|
+
const b = start({
|
|
145
|
+
reposBaseDir: repos,
|
|
146
|
+
enableStorageCleanup: true,
|
|
147
|
+
storageCleanupDeps: { cacheDirs: [cache], run: noGit, autoCleanupEnabled: true },
|
|
148
|
+
});
|
|
149
|
+
b.receive({ t: 'cleanup-auto' });
|
|
150
|
+
await until(() => b.ofType('cleanup-auto-result').length > 0);
|
|
151
|
+
const [r] = b.ofType('cleanup-auto-result');
|
|
152
|
+
expect(r).toMatchObject({ t: 'cleanup-auto-result', ran: true });
|
|
153
|
+
expect(r.freedBytes).toBeGreaterThanOrEqual(64 * 1024);
|
|
154
|
+
expect(fs.existsSync(cache)).toBe(false);
|
|
155
|
+
b.handle.close();
|
|
156
|
+
});
|
|
157
|
+
it('a bridge without cleanup answers ran:false', async () => {
|
|
158
|
+
const b = start();
|
|
159
|
+
b.receive({ t: 'cleanup-auto' });
|
|
160
|
+
await until(() => b.ofType('cleanup-auto-result').length > 0);
|
|
161
|
+
expect(b.ofType('cleanup-auto-result')).toEqual([{ t: 'cleanup-auto-result', ran: false, reason: 'disabled' }]);
|
|
162
|
+
b.handle.close();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe('stc-06 — readVolumeSpaceGb (the real statfs read)', () => {
|
|
166
|
+
it('reads a real volume: total > 0 and 0 ≤ free ≤ total', () => {
|
|
167
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
168
|
+
const { readVolumeSpaceGb } = require('../commands/bridge');
|
|
169
|
+
const r = readVolumeSpaceGb(os.tmpdir());
|
|
170
|
+
expect(r).not.toBeNull();
|
|
171
|
+
expect(r.totalGb).toBeGreaterThan(0);
|
|
172
|
+
expect(r.freeGb).toBeGreaterThanOrEqual(0);
|
|
173
|
+
expect(r.freeGb).toBeLessThanOrEqual(r.totalGb);
|
|
174
|
+
});
|
|
175
|
+
it('a missing path is unknown (null), never zero', () => {
|
|
176
|
+
const { readVolumeSpaceGb } = require('../commands/bridge');
|
|
177
|
+
expect(readVolumeSpaceGb(path.join(os.tmpdir(), 'no-such-dir-stc06', 'x'))).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -201,6 +201,15 @@ describe('PreviewRunner (tp-05) — real git, real processes', () => {
|
|
|
201
201
|
expect(fs.existsSync(path.join(fx.workingCopy, 'scratch.txt'))).toBe(true);
|
|
202
202
|
expect(git(fx.workingCopy, 'rev-parse', 'HEAD').trim()).not.toBe(v2);
|
|
203
203
|
});
|
|
204
|
+
it('stc-05 — activity() says what storage cleanup must not touch: in flight while starting, the serving workspace after, nothing once stopped', async () => {
|
|
205
|
+
expect(runner.activity()).toEqual({ opInFlight: false, activeWorkspaces: [] });
|
|
206
|
+
const starting = runner.start({ workspaceId: 'ws-1', repoUrl: fx.remote });
|
|
207
|
+
expect(runner.activity().opInFlight).toBe(true);
|
|
208
|
+
await starting;
|
|
209
|
+
expect(runner.activity()).toEqual({ opInFlight: false, activeWorkspaces: ['ws-1'] });
|
|
210
|
+
await runner.stop({ workspaceId: 'ws-1' });
|
|
211
|
+
expect(runner.activity()).toEqual({ opInFlight: false, activeWorkspaces: [] });
|
|
212
|
+
});
|
|
204
213
|
it('a broken build keeps the old sha serving and reports build_failed', async () => {
|
|
205
214
|
const first = await runner.start({ workspaceId: 'ws-1', repoUrl: fx.remote });
|
|
206
215
|
const v1 = first.runningSha;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|