@wenrwa/marketplace-cli 0.4.5 → 0.4.6
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-activity-parser.test.d.ts +1 -0
- package/dist/__tests__/bridge-activity-parser.test.js +0 -0
- package/dist/__tests__/bridge-activity-tap.test.d.ts +1 -0
- package/dist/__tests__/bridge-activity-tap.test.js +178 -0
- package/dist/__tests__/bridge-api-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-api-client.test.js +69 -0
- package/dist/__tests__/bridge-browser-driver.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-driver.test.js +379 -0
- package/dist/__tests__/bridge-browser-frames.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-frames.test.js +166 -0
- package/dist/__tests__/bridge-browser-idle.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-idle.test.js +259 -0
- package/dist/__tests__/bridge-browser-relay.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-relay.test.js +670 -0
- package/dist/__tests__/bridge-bundle-installer.test.d.ts +1 -0
- package/dist/__tests__/bridge-bundle-installer.test.js +126 -0
- package/dist/__tests__/bridge-cdp-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-cdp-client.test.js +133 -0
- package/dist/__tests__/bridge-chromium-launcher.test.d.ts +1 -0
- package/dist/__tests__/bridge-chromium-launcher.test.js +202 -0
- package/dist/__tests__/bridge-command-browser-options.test.d.ts +1 -0
- package/dist/__tests__/bridge-command-browser-options.test.js +37 -0
- package/dist/__tests__/bridge-config-mcp.test.js +4 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.js +125 -0
- package/dist/__tests__/bridge-context-writer.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-writer.test.js +161 -0
- package/dist/__tests__/bridge-device-registration.test.d.ts +1 -0
- package/dist/__tests__/bridge-device-registration.test.js +33 -0
- package/dist/__tests__/bridge-folder-picker-runner.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-picker-runner.test.js +37 -0
- package/dist/__tests__/bridge-folder-picker.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-picker.test.js +66 -0
- package/dist/__tests__/bridge-folder-registry.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-registry.test.js +107 -0
- package/dist/__tests__/bridge-frame-pacer.test.d.ts +1 -0
- package/dist/__tests__/bridge-frame-pacer.test.js +102 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.js +107 -0
- package/dist/__tests__/bridge-local-add-core.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-add-core.test.js +48 -0
- package/dist/__tests__/bridge-local-path-validation-fs.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-path-validation-fs.test.js +88 -0
- package/dist/__tests__/bridge-local-path-validation.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-path-validation.test.js +45 -0
- package/dist/__tests__/bridge-login-status.test.d.ts +1 -0
- package/dist/__tests__/bridge-login-status.test.js +60 -0
- package/dist/__tests__/bridge-relay-pucc14.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-pucc14.test.js +230 -0
- package/dist/__tests__/bridge-server-local-endpoints.test.d.ts +1 -0
- package/dist/__tests__/bridge-server-local-endpoints.test.js +197 -0
- package/dist/__tests__/bridge-server.test.js +22 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.d.ts +1 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.js +317 -0
- package/dist/__tests__/devices.test.d.ts +1 -0
- package/dist/__tests__/devices.test.js +66 -0
- package/dist/__tests__/local.test.d.ts +1 -0
- package/dist/__tests__/local.test.js +147 -0
- package/dist/bridge/activityParser.d.ts +53 -0
- package/dist/bridge/activityParser.js +202 -0
- package/dist/bridge/apiClient.d.ts +56 -0
- package/dist/bridge/apiClient.js +50 -0
- package/dist/bridge/browserDriver.d.ts +98 -0
- package/dist/bridge/browserDriver.js +265 -0
- package/dist/bridge/browserFrames.d.ts +182 -0
- package/dist/bridge/browserFrames.js +179 -0
- package/dist/bridge/browserIdle.d.ts +75 -0
- package/dist/bridge/browserIdle.js +120 -0
- package/dist/bridge/bundleInstaller.d.ts +31 -0
- package/dist/bridge/bundleInstaller.js +127 -0
- package/dist/bridge/cdpClient.d.ts +47 -0
- package/dist/bridge/cdpClient.js +102 -0
- package/dist/bridge/chromiumLauncher.d.ts +45 -0
- package/dist/bridge/chromiumLauncher.js +113 -0
- package/dist/bridge/config.d.ts +3 -0
- package/dist/bridge/contextWriter.d.ts +31 -0
- package/dist/bridge/contextWriter.js +139 -0
- package/dist/bridge/deviceRegistration.d.ts +28 -0
- package/dist/bridge/deviceRegistration.js +19 -0
- package/dist/bridge/folderPicker.d.ts +34 -0
- package/dist/bridge/folderPicker.js +36 -0
- package/dist/bridge/folderPickerRunner.d.ts +3 -0
- package/dist/bridge/folderPickerRunner.js +25 -0
- package/dist/bridge/folderRegistry.d.ts +19 -0
- package/dist/bridge/folderRegistry.js +100 -0
- package/dist/bridge/framePacer.d.ts +61 -0
- package/dist/bridge/framePacer.js +79 -0
- package/dist/bridge/localAdd.d.ts +28 -0
- package/dist/bridge/localAdd.js +67 -0
- package/dist/bridge/localPathValidation.d.ts +20 -0
- package/dist/bridge/localPathValidation.js +58 -0
- package/dist/bridge/localPathValidationFs.d.ts +2 -0
- package/dist/bridge/localPathValidationFs.js +98 -0
- package/dist/bridge/loginStatus.d.ts +38 -0
- package/dist/bridge/loginStatus.js +56 -0
- package/dist/bridge/realPty.js +6 -2
- package/dist/bridge/relayClient.d.ts +124 -0
- package/dist/bridge/relayClient.js +368 -5
- package/dist/bridge/repoCheckout.d.ts +31 -0
- package/dist/bridge/repoCheckout.js +18 -0
- package/dist/bridge/server.d.ts +33 -0
- package/dist/bridge/server.js +98 -10
- package/dist/bridge/sessionManager.d.ts +18 -0
- package/dist/bridge/sessionManager.js +15 -1
- package/dist/commands/bridge.d.ts +6 -0
- package/dist/commands/bridge.js +155 -1
- package/dist/commands/devices.d.ts +9 -0
- package/dist/commands/devices.js +53 -0
- package/dist/commands/local.d.ts +9 -0
- package/dist/commands/local.js +141 -0
- package/dist/index.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* TDD — lws-05: the three loopback HTTP endpoints backing the arena
|
|
5
|
+
* create-modal's local mode (PRD D3): POST /local/validate-path,
|
|
6
|
+
* POST /local/pick-folder, POST /local/add. Same server as bridge-server
|
|
7
|
+
* .test.ts (discovery/WS) — split into its own file since these are a
|
|
8
|
+
* distinct vertical slice. All handlers are injected opts (no real fs,
|
|
9
|
+
* osascript, or network — per the issue's "no real osascript in tests").
|
|
10
|
+
*/
|
|
11
|
+
const server_1 = require("../bridge/server");
|
|
12
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
13
|
+
class FakePty {
|
|
14
|
+
write() { }
|
|
15
|
+
resize() { }
|
|
16
|
+
kill() { }
|
|
17
|
+
onData() { }
|
|
18
|
+
onExit() { }
|
|
19
|
+
}
|
|
20
|
+
const ORIGIN = 'https://app.hiveop.io';
|
|
21
|
+
const TOKEN = 'session-token-123';
|
|
22
|
+
const factory = { spawn: () => new FakePty() };
|
|
23
|
+
async function postJson(port, path, body, opts = {}) {
|
|
24
|
+
const headers = {
|
|
25
|
+
'content-type': 'application/json',
|
|
26
|
+
origin: opts.origin ?? ORIGIN,
|
|
27
|
+
};
|
|
28
|
+
if (opts.sessionToken !== undefined)
|
|
29
|
+
headers['x-bridge-session-token'] = opts.sessionToken;
|
|
30
|
+
const res = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers,
|
|
33
|
+
body: JSON.stringify(body),
|
|
34
|
+
});
|
|
35
|
+
return { status: res.status, json: await res.json().catch(() => null) };
|
|
36
|
+
}
|
|
37
|
+
describe('POST /local/validate-path', () => {
|
|
38
|
+
let handle;
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
await handle.close();
|
|
41
|
+
});
|
|
42
|
+
it('returns the validator result for a valid folder', async () => {
|
|
43
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
44
|
+
sessionToken: TOKEN,
|
|
45
|
+
allowedOrigin: ORIGIN,
|
|
46
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
47
|
+
validatePath: async () => ({ ok: true, name: 'my-app', fileCount: 214, isGitRepo: true }),
|
|
48
|
+
});
|
|
49
|
+
const { status, json } = await postJson(handle.port, '/local/validate-path', { path: '/Users/x/my-app' });
|
|
50
|
+
expect(status).toBe(200);
|
|
51
|
+
expect(json).toEqual({ ok: true, name: 'my-app', fileCount: 214, isGitRepo: true });
|
|
52
|
+
});
|
|
53
|
+
it('returns a typed error for an invalid path', async () => {
|
|
54
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
55
|
+
sessionToken: TOKEN,
|
|
56
|
+
allowedOrigin: ORIGIN,
|
|
57
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
58
|
+
validatePath: async () => ({ ok: false, error: 'not-exists' }),
|
|
59
|
+
});
|
|
60
|
+
const { status, json } = await postJson(handle.port, '/local/validate-path', { path: '/nope' });
|
|
61
|
+
expect(status).toBe(200);
|
|
62
|
+
expect(json).toEqual({ ok: false, error: 'not-exists' });
|
|
63
|
+
});
|
|
64
|
+
it('rejects a request from a non-arena Origin', async () => {
|
|
65
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
66
|
+
sessionToken: TOKEN,
|
|
67
|
+
allowedOrigin: ORIGIN,
|
|
68
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
69
|
+
validatePath: async () => ({ ok: true, name: 'x', fileCount: 1, isGitRepo: false }),
|
|
70
|
+
});
|
|
71
|
+
const { status } = await postJson(handle.port, '/local/validate-path', { path: '/x' }, { origin: 'https://evil.example' });
|
|
72
|
+
expect(status).toBe(403);
|
|
73
|
+
});
|
|
74
|
+
it('500s cleanly when the endpoint is not wired', async () => {
|
|
75
|
+
handle = await (0, server_1.startBridgeServer)({ sessionToken: TOKEN, allowedOrigin: ORIGIN, sessions: new sessionManager_1.SessionManager(factory) });
|
|
76
|
+
const { status } = await postJson(handle.port, '/local/validate-path', { path: '/x' });
|
|
77
|
+
expect(status).toBe(500);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('POST /local/pick-folder', () => {
|
|
81
|
+
let handle;
|
|
82
|
+
afterEach(async () => {
|
|
83
|
+
await handle.close();
|
|
84
|
+
});
|
|
85
|
+
it('returns the chosen path', async () => {
|
|
86
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
87
|
+
sessionToken: TOKEN,
|
|
88
|
+
allowedOrigin: ORIGIN,
|
|
89
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
90
|
+
pickFolder: async () => ({ path: '/Users/x/my-app' }),
|
|
91
|
+
});
|
|
92
|
+
const { status, json } = await postJson(handle.port, '/local/pick-folder', {});
|
|
93
|
+
expect(status).toBe(200);
|
|
94
|
+
expect(json).toEqual({ path: '/Users/x/my-app' });
|
|
95
|
+
});
|
|
96
|
+
it('surfaces {cancelled:true} when the user dismisses the dialog', async () => {
|
|
97
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
98
|
+
sessionToken: TOKEN,
|
|
99
|
+
allowedOrigin: ORIGIN,
|
|
100
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
101
|
+
pickFolder: async () => ({ cancelled: true }),
|
|
102
|
+
});
|
|
103
|
+
const { json } = await postJson(handle.port, '/local/pick-folder', {});
|
|
104
|
+
expect(json).toEqual({ cancelled: true });
|
|
105
|
+
});
|
|
106
|
+
it('surfaces {unsupported:true} on platforms with no native picker', async () => {
|
|
107
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
108
|
+
sessionToken: TOKEN,
|
|
109
|
+
allowedOrigin: ORIGIN,
|
|
110
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
111
|
+
pickFolder: async () => ({ unsupported: true }),
|
|
112
|
+
});
|
|
113
|
+
const { json } = await postJson(handle.port, '/local/pick-folder', {});
|
|
114
|
+
expect(json).toEqual({ unsupported: true });
|
|
115
|
+
});
|
|
116
|
+
it('one picker at a time — a second concurrent call gets 409 busy without invoking the picker again', async () => {
|
|
117
|
+
let resolvePick;
|
|
118
|
+
const pending = new Promise((resolve) => { resolvePick = resolve; });
|
|
119
|
+
const pickFolder = jest.fn().mockReturnValue(pending);
|
|
120
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
121
|
+
sessionToken: TOKEN,
|
|
122
|
+
allowedOrigin: ORIGIN,
|
|
123
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
124
|
+
pickFolder,
|
|
125
|
+
});
|
|
126
|
+
const first = postJson(handle.port, '/local/pick-folder', {});
|
|
127
|
+
await new Promise((r) => setTimeout(r, 10)); // let the first request start the picker
|
|
128
|
+
const second = await postJson(handle.port, '/local/pick-folder', {});
|
|
129
|
+
expect(second.status).toBe(409);
|
|
130
|
+
expect(second.json).toEqual({ busy: true });
|
|
131
|
+
expect(pickFolder).toHaveBeenCalledTimes(1);
|
|
132
|
+
resolvePick({ path: '/Users/x/app' });
|
|
133
|
+
const firstResult = await first;
|
|
134
|
+
expect(firstResult.json).toEqual({ path: '/Users/x/app' });
|
|
135
|
+
// The gate releases after completion — a third call runs the picker again.
|
|
136
|
+
const third = await postJson(handle.port, '/local/pick-folder', {});
|
|
137
|
+
expect(pickFolder).toHaveBeenCalledTimes(2);
|
|
138
|
+
expect(third.status).toBe(200);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe('POST /local/add', () => {
|
|
142
|
+
let handle;
|
|
143
|
+
afterEach(async () => {
|
|
144
|
+
await handle.close();
|
|
145
|
+
});
|
|
146
|
+
it('requires the session token — missing/wrong is rejected with 401', async () => {
|
|
147
|
+
const addLocalWorkspace = jest.fn();
|
|
148
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
149
|
+
sessionToken: TOKEN,
|
|
150
|
+
allowedOrigin: ORIGIN,
|
|
151
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
152
|
+
addLocalWorkspace,
|
|
153
|
+
});
|
|
154
|
+
const missing = await postJson(handle.port, '/local/add', { path: '/x' });
|
|
155
|
+
expect(missing.status).toBe(401);
|
|
156
|
+
const wrong = await postJson(handle.port, '/local/add', { path: '/x' }, { sessionToken: 'wrong' });
|
|
157
|
+
expect(wrong.status).toBe(401);
|
|
158
|
+
expect(addLocalWorkspace).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
it('creates and returns the workspace summary with a valid session token', async () => {
|
|
161
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
162
|
+
sessionToken: TOKEN,
|
|
163
|
+
allowedOrigin: ORIGIN,
|
|
164
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
165
|
+
addLocalWorkspace: async ({ path, name }) => {
|
|
166
|
+
expect(path).toBe('/Users/x/my-app');
|
|
167
|
+
expect(name).toBeUndefined();
|
|
168
|
+
return { status: 'created', workspace: { id: 'ws-1', name: 'my-app' } };
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
const { status, json } = await postJson(handle.port, '/local/add', { path: '/Users/x/my-app' }, { sessionToken: TOKEN });
|
|
172
|
+
expect(status).toBe(201);
|
|
173
|
+
expect(json).toEqual({ ok: true, status: 'created', workspace: { id: 'ws-1', name: 'my-app' } });
|
|
174
|
+
});
|
|
175
|
+
it('is idempotent — an already-registered path returns 200 existing, not 201', async () => {
|
|
176
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
177
|
+
sessionToken: TOKEN,
|
|
178
|
+
allowedOrigin: ORIGIN,
|
|
179
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
180
|
+
addLocalWorkspace: async () => ({ status: 'existing', workspace: { id: 'ws-1', name: 'my-app' } }),
|
|
181
|
+
});
|
|
182
|
+
const { status, json } = await postJson(handle.port, '/local/add', { path: '/x' }, { sessionToken: TOKEN });
|
|
183
|
+
expect(status).toBe(200);
|
|
184
|
+
expect(json).toEqual({ ok: true, status: 'existing', workspace: { id: 'ws-1', name: 'my-app' } });
|
|
185
|
+
});
|
|
186
|
+
it('surfaces a creation error as ok:false without a 2xx status', async () => {
|
|
187
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
188
|
+
sessionToken: TOKEN,
|
|
189
|
+
allowedOrigin: ORIGIN,
|
|
190
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
191
|
+
addLocalWorkspace: async () => ({ status: 'error', message: 'device offline' }),
|
|
192
|
+
});
|
|
193
|
+
const { status, json } = await postJson(handle.port, '/local/add', { path: '/x' }, { sessionToken: TOKEN });
|
|
194
|
+
expect(status).toBe(500);
|
|
195
|
+
expect(json).toEqual({ ok: false, message: 'device offline' });
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -125,6 +125,28 @@ describe('bridge WebSocket server', () => {
|
|
|
125
125
|
expect(bad.status).toBe(403);
|
|
126
126
|
expect(bad.headers.get('access-control-allow-origin')).toBeNull();
|
|
127
127
|
});
|
|
128
|
+
it('lws-03: discovery advertises registered local workspaces, empty by default', async () => {
|
|
129
|
+
const res = await fetch(`http://127.0.0.1:${handle.port}/discovery`, { headers: { origin: ORIGIN } });
|
|
130
|
+
const body = await res.json();
|
|
131
|
+
expect(body.localWorkspaces).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
it('lws-03: discovery reflects the registry live — no restart needed — via a getter closure', async () => {
|
|
134
|
+
await handle.close();
|
|
135
|
+
const factory = { spawn: () => { const p = new FakePty(); ptys.push(p); return p; } };
|
|
136
|
+
let entries = [];
|
|
137
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
138
|
+
sessionToken: TOKEN,
|
|
139
|
+
allowedOrigin: ORIGIN,
|
|
140
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
141
|
+
getLocalWorkspaces: () => entries,
|
|
142
|
+
});
|
|
143
|
+
const before = await (await fetch(`http://127.0.0.1:${handle.port}/discovery`, { headers: { origin: ORIGIN } })).json();
|
|
144
|
+
expect(before.localWorkspaces).toEqual([]);
|
|
145
|
+
// Simulate `wen-market local add` mutating the registry mid-run.
|
|
146
|
+
entries = [{ workspaceId: 'ws-1', name: 'my-app', path: 'my-app' }];
|
|
147
|
+
const after = await (await fetch(`http://127.0.0.1:${handle.port}/discovery`, { headers: { origin: ORIGIN } })).json();
|
|
148
|
+
expect(after.localWorkspaces).toEqual([{ workspaceId: 'ws-1', name: 'my-app', path: 'my-app' }]);
|
|
149
|
+
});
|
|
128
150
|
it('answers the Chrome Private-Network-Access preflight for the arena origin', async () => {
|
|
129
151
|
const res = await fetch(`http://127.0.0.1:${handle.port}/discovery`, {
|
|
130
152
|
method: 'OPTIONS',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,317 @@
|
|
|
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
|
+
* pucc-03 — model B multi-repo + per-terminal workspace cwd/env (ADR-0031).
|
|
38
|
+
*
|
|
39
|
+
* One container holds many workspaces' repos under /data/repos/<workspaceId>,
|
|
40
|
+
* cloned on demand the first time a terminal for that workspace opens. Every
|
|
41
|
+
* workspace-scoped terminal carries WENRWA_WORKSPACE_ID (and a per-session
|
|
42
|
+
* HT_AUTONOMOUS) in its PTY env so downstream tooling (pucc-04) can resolve
|
|
43
|
+
* the workspace inside the shared box. This suite pins that contract with the
|
|
44
|
+
* existing faked-PTY / fake-relay harness — no docker, no real shells.
|
|
45
|
+
*/
|
|
46
|
+
const os = __importStar(require("os"));
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const child_process_1 = require("child_process");
|
|
50
|
+
const ws_1 = require("ws");
|
|
51
|
+
const relayClient_1 = require("../bridge/relayClient");
|
|
52
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
53
|
+
const repoCheckout_1 = require("../bridge/repoCheckout");
|
|
54
|
+
class FakePty {
|
|
55
|
+
opts;
|
|
56
|
+
written = [];
|
|
57
|
+
constructor(opts) {
|
|
58
|
+
this.opts = opts;
|
|
59
|
+
}
|
|
60
|
+
write(data) { this.written.push(data); }
|
|
61
|
+
resize() { }
|
|
62
|
+
kill() { }
|
|
63
|
+
dataCb = null;
|
|
64
|
+
onData(cb) { this.dataCb = cb; }
|
|
65
|
+
onExit(_cb) { }
|
|
66
|
+
emitData(d) { this.dataCb?.(d); }
|
|
67
|
+
}
|
|
68
|
+
const makeFactory = () => {
|
|
69
|
+
const ptys = [];
|
|
70
|
+
const factory = {
|
|
71
|
+
spawn: (opts) => {
|
|
72
|
+
const p = new FakePty(opts);
|
|
73
|
+
ptys.push(p);
|
|
74
|
+
return p;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
return { factory, ptys };
|
|
78
|
+
};
|
|
79
|
+
// ── fake-relay + frame-queue helpers (mirror bridge-relay-client.test.ts) ──
|
|
80
|
+
function frameQueue(ws) {
|
|
81
|
+
const frames = [];
|
|
82
|
+
const waiters = [];
|
|
83
|
+
ws.on('message', (raw) => {
|
|
84
|
+
const f = JSON.parse(String(raw));
|
|
85
|
+
const w = waiters.shift();
|
|
86
|
+
if (w)
|
|
87
|
+
w(f);
|
|
88
|
+
else
|
|
89
|
+
frames.push(f);
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
next: () => frames.length > 0 ? Promise.resolve(frames.shift()) : new Promise((r) => waiters.push(r)),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function startFakeRelay() {
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
const wss = new ws_1.WebSocketServer({ port: 0, host: '127.0.0.1' });
|
|
98
|
+
const queue = [];
|
|
99
|
+
const waiters = [];
|
|
100
|
+
wss.on('connection', (ws) => {
|
|
101
|
+
const w = waiters.shift();
|
|
102
|
+
if (w)
|
|
103
|
+
w(ws);
|
|
104
|
+
else
|
|
105
|
+
queue.push(ws);
|
|
106
|
+
});
|
|
107
|
+
wss.on('listening', () => {
|
|
108
|
+
const addr = wss.address();
|
|
109
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
110
|
+
resolve({
|
|
111
|
+
wss,
|
|
112
|
+
port,
|
|
113
|
+
nextConnection: () => (queue.length > 0 ? Promise.resolve(queue.shift()) : new Promise((r) => waiters.push(r))),
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function closeRelay(wss) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
for (const client of wss.clients)
|
|
121
|
+
client.terminate();
|
|
122
|
+
wss.close((err) => (err ? reject(err) : resolve()));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// ── ensureWorkspaceRepo: /data/repos/<workspaceId> clone-on-demand ──────
|
|
126
|
+
describe('ensureWorkspaceRepo — per-workspace clone-on-demand (ADR-0031 layout)', () => {
|
|
127
|
+
const REPOS = '/data/repos';
|
|
128
|
+
const makeDeps = (existing = [], cloneFails = false) => {
|
|
129
|
+
const runs = [];
|
|
130
|
+
return {
|
|
131
|
+
runs,
|
|
132
|
+
deps: {
|
|
133
|
+
reposBaseDir: REPOS,
|
|
134
|
+
exists: (p) => existing.includes(p),
|
|
135
|
+
mkdir: (_p) => { },
|
|
136
|
+
run: async (cmd, args) => {
|
|
137
|
+
runs.push([cmd, ...args]);
|
|
138
|
+
if (cloneFails)
|
|
139
|
+
throw new Error('clone failed');
|
|
140
|
+
return { stdout: '', code: 0 };
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
it('clones a missing workspace repo to /data/repos/<workspaceId>', async () => {
|
|
146
|
+
const { deps, runs } = makeDeps();
|
|
147
|
+
const result = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-42', repoUrl: 'https://github.com/acme/widgets.git' }, deps);
|
|
148
|
+
expect(result).toEqual({ path: `${REPOS}/ws-42`, action: 'cloned' });
|
|
149
|
+
expect(runs[0].join(' ')).toBe(`git clone https://github.com/acme/widgets.git ${REPOS}/ws-42`);
|
|
150
|
+
});
|
|
151
|
+
it('reuses an existing checkout (.git present) without running git — no re-clone', async () => {
|
|
152
|
+
const { deps, runs } = makeDeps([`${REPOS}/ws-42/.git`]);
|
|
153
|
+
const result = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-42', repoUrl: 'https://github.com/acme/widgets.git' }, deps);
|
|
154
|
+
expect(result).toEqual({ path: `${REPOS}/ws-42`, action: 'existing' });
|
|
155
|
+
expect(runs).toHaveLength(0);
|
|
156
|
+
});
|
|
157
|
+
it('is keyed by workspaceId, not repo name (distinct workspaces → distinct dirs)', async () => {
|
|
158
|
+
const { deps } = makeDeps();
|
|
159
|
+
const a = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'alpha', repoUrl: 'https://github.com/acme/widgets.git' }, deps);
|
|
160
|
+
const b = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'beta', repoUrl: 'https://github.com/acme/widgets.git' }, deps);
|
|
161
|
+
expect(a.path).toBe(`${REPOS}/alpha`);
|
|
162
|
+
expect(b.path).toBe(`${REPOS}/beta`);
|
|
163
|
+
});
|
|
164
|
+
it('reports failure (→ terminal falls back to home) when there is no url and no checkout', async () => {
|
|
165
|
+
const { deps, runs } = makeDeps();
|
|
166
|
+
const result = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-42' }, deps);
|
|
167
|
+
expect(result.action).toBe('failed');
|
|
168
|
+
expect(runs).toHaveLength(0);
|
|
169
|
+
});
|
|
170
|
+
it('reports failure without throwing when the clone fails', async () => {
|
|
171
|
+
const { deps } = makeDeps([], true);
|
|
172
|
+
const result = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-42', repoUrl: 'https://github.com/p/r' }, deps);
|
|
173
|
+
expect(result.action).toBe('failed');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
// ── SessionManager: per-session env reaches the PTY factory ─────────────
|
|
177
|
+
describe('SessionManager — per-session env passthrough (pucc-03)', () => {
|
|
178
|
+
it('forwards env to the PTY factory spawn opts', () => {
|
|
179
|
+
const { factory, ptys } = makeFactory();
|
|
180
|
+
const sm = new sessionManager_1.SessionManager(factory);
|
|
181
|
+
sm.open({ cols: 80, rows: 24, cwd: '/data/repos/ws-1', env: { WENRWA_WORKSPACE_ID: 'ws-1' } });
|
|
182
|
+
expect(ptys[0].opts.cwd).toBe('/data/repos/ws-1');
|
|
183
|
+
expect(ptys[0].opts.env).toEqual({ WENRWA_WORKSPACE_ID: 'ws-1' });
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
// ── relay client: the workspaceId → cwd + env contract pucc-04 builds on ──
|
|
187
|
+
describe('relay client — workspace-scoped open (pucc-03 contract)', () => {
|
|
188
|
+
it('open with workspaceId+repoUrl spawns in the resolved cwd with WENRWA_WORKSPACE_ID env, echoes workspaceId', async () => {
|
|
189
|
+
const relay = await startFakeRelay();
|
|
190
|
+
const { factory, ptys } = makeFactory();
|
|
191
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
192
|
+
const resolveCalls = [];
|
|
193
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
194
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
195
|
+
relayToken: 'tok',
|
|
196
|
+
sessions,
|
|
197
|
+
resolveWorkspaceCwd: async (o) => {
|
|
198
|
+
resolveCalls.push(o);
|
|
199
|
+
return `/data/repos/${o.workspaceId}`;
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const ws = await relay.nextConnection();
|
|
203
|
+
const q = frameQueue(ws);
|
|
204
|
+
await q.next(); // auth
|
|
205
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, workspaceId: 'ws-7', repoUrl: 'https://github.com/acme/w.git' }));
|
|
206
|
+
const opened = await q.next();
|
|
207
|
+
expect(opened.t).toBe('opened');
|
|
208
|
+
expect(opened.workspaceId).toBe('ws-7');
|
|
209
|
+
expect(opened.cwd).toBe('/data/repos/ws-7');
|
|
210
|
+
expect(resolveCalls).toEqual([{ workspaceId: 'ws-7', repoUrl: 'https://github.com/acme/w.git' }]);
|
|
211
|
+
expect(ptys[0].opts.cwd).toBe('/data/repos/ws-7');
|
|
212
|
+
expect(ptys[0].opts.env).toMatchObject({ WENRWA_WORKSPACE_ID: 'ws-7', HT_AUTONOMOUS: '0' });
|
|
213
|
+
handle.close();
|
|
214
|
+
await closeRelay(relay.wss);
|
|
215
|
+
});
|
|
216
|
+
it('autonomous:true selects HT_AUTONOMOUS=1 per session (claude --dangerously-skip-permissions)', async () => {
|
|
217
|
+
const relay = await startFakeRelay();
|
|
218
|
+
const { factory, ptys } = makeFactory();
|
|
219
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
220
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
221
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
222
|
+
relayToken: 'tok',
|
|
223
|
+
sessions,
|
|
224
|
+
resolveWorkspaceCwd: async ({ workspaceId }) => `/data/repos/${workspaceId}`,
|
|
225
|
+
});
|
|
226
|
+
const ws = await relay.nextConnection();
|
|
227
|
+
const q = frameQueue(ws);
|
|
228
|
+
await q.next();
|
|
229
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, workspaceId: 'ws-9', autonomous: true }));
|
|
230
|
+
await q.next();
|
|
231
|
+
expect(ptys[0].opts.env).toMatchObject({ WENRWA_WORKSPACE_ID: 'ws-9', HT_AUTONOMOUS: '1' });
|
|
232
|
+
handle.close();
|
|
233
|
+
await closeRelay(relay.wss);
|
|
234
|
+
});
|
|
235
|
+
it('sets WENRWA_WORKSPACE_ID even when the checkout is unavailable (cwd falls back to home)', async () => {
|
|
236
|
+
const relay = await startFakeRelay();
|
|
237
|
+
const { factory, ptys } = makeFactory();
|
|
238
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
239
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
240
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
241
|
+
relayToken: 'tok',
|
|
242
|
+
sessions,
|
|
243
|
+
resolveWorkspaceCwd: async () => null, // clone failed / no url
|
|
244
|
+
});
|
|
245
|
+
const ws = await relay.nextConnection();
|
|
246
|
+
const q = frameQueue(ws);
|
|
247
|
+
await q.next();
|
|
248
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, workspaceId: 'ws-x' }));
|
|
249
|
+
const opened = await q.next();
|
|
250
|
+
expect(opened.workspaceId).toBe('ws-x');
|
|
251
|
+
expect(opened.cwd).toBeUndefined();
|
|
252
|
+
expect(ptys[0].opts.cwd).toBeUndefined();
|
|
253
|
+
expect(ptys[0].opts.env).toMatchObject({ WENRWA_WORKSPACE_ID: 'ws-x' });
|
|
254
|
+
handle.close();
|
|
255
|
+
await closeRelay(relay.wss);
|
|
256
|
+
});
|
|
257
|
+
it('legacy fallback: open WITHOUT workspaceId spawns a plain shell (no cwd, no injected env)', async () => {
|
|
258
|
+
const relay = await startFakeRelay();
|
|
259
|
+
const { factory, ptys } = makeFactory();
|
|
260
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
261
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
262
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
263
|
+
relayToken: 'tok',
|
|
264
|
+
sessions,
|
|
265
|
+
resolveWorkspaceCwd: async () => '/should/not/be/used',
|
|
266
|
+
});
|
|
267
|
+
const ws = await relay.nextConnection();
|
|
268
|
+
const q = frameQueue(ws);
|
|
269
|
+
await q.next();
|
|
270
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
|
|
271
|
+
const opened = await q.next();
|
|
272
|
+
expect(opened.t).toBe('opened');
|
|
273
|
+
expect(opened.workspaceId).toBeUndefined();
|
|
274
|
+
expect(ptys[0].opts.cwd).toBeUndefined();
|
|
275
|
+
expect(ptys[0].opts.env).toBeUndefined();
|
|
276
|
+
handle.close();
|
|
277
|
+
await closeRelay(relay.wss);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
/**
|
|
281
|
+
* Env-gated integration (RUN_HT_INTEGRATION): proves clone-on-demand + reuse
|
|
282
|
+
* against a REAL git binary and temp dirs — no docker required, but skipped by
|
|
283
|
+
* default so CI/this-machine never blocks on it. The full docker-level check
|
|
284
|
+
* (RUN_HT_DOCKER) lands in the deploy/verify stage (pucc-09 cutover), where an
|
|
285
|
+
* actual image + Fly volume exist to exercise the shared-container path.
|
|
286
|
+
*/
|
|
287
|
+
const runIntegration = process.env.RUN_HT_INTEGRATION === '1';
|
|
288
|
+
(runIntegration ? describe : describe.skip)('ensureWorkspaceRepo — real git clone-on-demand (env-gated)', () => {
|
|
289
|
+
it('clones on first call then reuses the checkout on the second (idempotent)', async () => {
|
|
290
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pucc03-'));
|
|
291
|
+
const origin = path.join(root, 'origin.git');
|
|
292
|
+
const repos = path.join(root, 'repos');
|
|
293
|
+
(0, child_process_1.execFileSync)('git', ['init', '--bare', origin]);
|
|
294
|
+
// seed the bare origin with one commit so `git clone` produces a checkout
|
|
295
|
+
const seed = path.join(root, 'seed');
|
|
296
|
+
(0, child_process_1.execFileSync)('git', ['clone', origin, seed]);
|
|
297
|
+
fs.writeFileSync(path.join(seed, 'README.md'), '# hi\n');
|
|
298
|
+
(0, child_process_1.execFileSync)('git', ['-C', seed, 'add', '.']);
|
|
299
|
+
(0, child_process_1.execFileSync)('git', ['-C', seed, '-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-m', 'init']);
|
|
300
|
+
(0, child_process_1.execFileSync)('git', ['-C', seed, 'push', 'origin', 'HEAD:refs/heads/main']);
|
|
301
|
+
const deps = {
|
|
302
|
+
reposBaseDir: repos,
|
|
303
|
+
exists: (p) => fs.existsSync(p),
|
|
304
|
+
mkdir: (p) => fs.mkdirSync(p, { recursive: true }),
|
|
305
|
+
run: async (cmd, args) => {
|
|
306
|
+
(0, child_process_1.execFileSync)(cmd, args);
|
|
307
|
+
return { stdout: '', code: 0 };
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
const first = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-int', repoUrl: origin }, deps);
|
|
311
|
+
expect(first.action).toBe('cloned');
|
|
312
|
+
expect(fs.existsSync(path.join(repos, 'ws-int', '.git'))).toBe(true);
|
|
313
|
+
const second = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-int', repoUrl: origin }, deps);
|
|
314
|
+
expect(second.action).toBe('existing');
|
|
315
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
/**
|
|
7
|
+
* TDD — lws-03: `wen-market devices` list/revoke. Uses this machine's own
|
|
8
|
+
* Connect Token (bridge-config.json) — no interactive login required.
|
|
9
|
+
*/
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
const mockLoadBridgeConfig = jest.fn();
|
|
12
|
+
jest.mock('../bridge/config', () => ({
|
|
13
|
+
loadBridgeConfig: () => mockLoadBridgeConfig(),
|
|
14
|
+
}));
|
|
15
|
+
const mockListDevices = jest.fn();
|
|
16
|
+
const mockRevokeDevice = jest.fn();
|
|
17
|
+
jest.mock('../bridge/apiClient', () => ({
|
|
18
|
+
listDevices: (...args) => mockListDevices(...args),
|
|
19
|
+
revokeDevice: (...args) => mockRevokeDevice(...args),
|
|
20
|
+
}));
|
|
21
|
+
jest.mock('../commands/bridge', () => ({ bridgeApiUrl: () => 'https://api.x' }));
|
|
22
|
+
const devices_1 = require("../commands/devices");
|
|
23
|
+
async function runCommand(...args) {
|
|
24
|
+
await devices_1.devicesCommand.parseAsync(args, { from: 'user' });
|
|
25
|
+
}
|
|
26
|
+
beforeAll(() => {
|
|
27
|
+
chalk_1.default.level = 0;
|
|
28
|
+
});
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
mockLoadBridgeConfig.mockReturnValue({ connectToken: 'wm_ct_' + 'a'.repeat(64), deviceId: 'd-this' });
|
|
32
|
+
});
|
|
33
|
+
describe('devices list', () => {
|
|
34
|
+
it('lists devices and marks the current one', async () => {
|
|
35
|
+
mockListDevices.mockResolvedValue({ success: true, devices: [
|
|
36
|
+
{ id: 'd-this', name: 'bunny-mbp', revokedAt: null },
|
|
37
|
+
{ id: 'd-other', name: 'other-box', revokedAt: null },
|
|
38
|
+
] });
|
|
39
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
40
|
+
await runCommand();
|
|
41
|
+
const output = spy.mock.calls.map((c) => c[0]).join('\n');
|
|
42
|
+
expect(output).toContain('bunny-mbp');
|
|
43
|
+
expect(output).toContain('other-box');
|
|
44
|
+
expect(output).toContain('this machine');
|
|
45
|
+
spy.mockRestore();
|
|
46
|
+
});
|
|
47
|
+
it('errors clearly with no Connect Token configured', async () => {
|
|
48
|
+
mockLoadBridgeConfig.mockReturnValue(null);
|
|
49
|
+
const errSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
50
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
51
|
+
await runCommand();
|
|
52
|
+
expect(mockListDevices).not.toHaveBeenCalled();
|
|
53
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
54
|
+
errSpy.mockRestore();
|
|
55
|
+
exitSpy.mockRestore();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('devices revoke', () => {
|
|
59
|
+
it('revokes a device by id', async () => {
|
|
60
|
+
mockRevokeDevice.mockResolvedValue({ success: true });
|
|
61
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
62
|
+
await runCommand('revoke', 'd-other');
|
|
63
|
+
expect(mockRevokeDevice).toHaveBeenCalledWith(expect.anything(), 'd-other');
|
|
64
|
+
spy.mockRestore();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|