@wenrwa/marketplace-cli 0.4.4 → 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-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-client.test.js +283 -0
- package/dist/__tests__/bridge-relay-mode.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-mode.test.js +33 -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 +166 -0
- package/dist/bridge/relayClient.js +517 -0
- 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 +21 -0
- package/dist/commands/bridge.js +194 -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 +7 -1
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* TDD — lws-05: shared idempotent-add core behind POST /local/add (the
|
|
5
|
+
* bridge-side twin of `wen-market local add`, PRD D3). Deps-injected — no
|
|
6
|
+
* real fs/network — server.ts wires the real folderRegistry + apiClient.
|
|
7
|
+
*/
|
|
8
|
+
const localAdd_1 = require("../bridge/localAdd");
|
|
9
|
+
function deps(registry, overrides = {}) {
|
|
10
|
+
let state = registry;
|
|
11
|
+
return {
|
|
12
|
+
loadRegistry: () => state,
|
|
13
|
+
saveRegistry: (r) => { state = r; },
|
|
14
|
+
createWorkspace: jest.fn().mockResolvedValue({ workspace: { id: 'ws-1', name: 'my-app' } }),
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
describe('addLocalWorkspaceCore', () => {
|
|
19
|
+
it('creates a new workspace and registers the path when not already registered', async () => {
|
|
20
|
+
const d = deps({ v: 1, entries: {} });
|
|
21
|
+
const result = await (0, localAdd_1.addLocalWorkspaceCore)('/Users/x/my-app', undefined, d);
|
|
22
|
+
expect(result).toEqual({ status: 'created', workspace: { id: 'ws-1', name: 'my-app' } });
|
|
23
|
+
expect(d.createWorkspace).toHaveBeenCalledWith('my-app');
|
|
24
|
+
expect(d.loadRegistry().entries['/Users/x/my-app']).toMatchObject({ workspaceId: 'ws-1', name: 'my-app' });
|
|
25
|
+
});
|
|
26
|
+
it('is idempotent — re-adding the same path returns the existing workspace without creating a new one', async () => {
|
|
27
|
+
const d = deps({
|
|
28
|
+
v: 1,
|
|
29
|
+
entries: { '/Users/x/my-app': { workspaceId: 'ws-existing', name: 'my-app', addedAt: 'now' } },
|
|
30
|
+
});
|
|
31
|
+
const result = await (0, localAdd_1.addLocalWorkspaceCore)('/Users/x/my-app', undefined, d);
|
|
32
|
+
expect(result).toEqual({ status: 'existing', workspace: { id: 'ws-existing', name: 'my-app' } });
|
|
33
|
+
expect(d.createWorkspace).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
it('uses a name override when provided, falling back to basename otherwise', async () => {
|
|
36
|
+
const d = deps({ v: 1, entries: {} });
|
|
37
|
+
await (0, localAdd_1.addLocalWorkspaceCore)('/Users/x/my-app', 'Custom Name', d);
|
|
38
|
+
expect(d.createWorkspace).toHaveBeenCalledWith('Custom Name');
|
|
39
|
+
});
|
|
40
|
+
it('surfaces a typed error without leaving a half-registered entry when creation fails', async () => {
|
|
41
|
+
const d = deps({ v: 1, entries: {} }, {
|
|
42
|
+
createWorkspace: jest.fn().mockRejectedValue(new Error('device offline')),
|
|
43
|
+
});
|
|
44
|
+
const result = await (0, localAdd_1.addLocalWorkspaceCore)('/Users/x/my-app', undefined, d);
|
|
45
|
+
expect(result).toEqual({ status: 'error', message: 'device offline' });
|
|
46
|
+
expect(d.loadRegistry().entries['/Users/x/my-app']).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
* TDD — lws-05: the real fs-backed LocalPathDeps (node:fs), used by
|
|
38
|
+
* commands/bridge.ts to wire POST /local/validate-path. Exercises actual
|
|
39
|
+
* temp directories — no mocking fs itself (prior art: bridge-repo-checkout
|
|
40
|
+
* tests use real tmp dirs for the same reason: path/fs edge cases are worth
|
|
41
|
+
* testing for real).
|
|
42
|
+
*/
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const os = __importStar(require("os"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const localPathValidationFs_1 = require("../bridge/localPathValidationFs");
|
|
47
|
+
const localPathValidation_1 = require("../bridge/localPathValidation");
|
|
48
|
+
describe('realLocalPathDeps (fs-backed)', () => {
|
|
49
|
+
let dir;
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lws05-validate-'));
|
|
52
|
+
});
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
it('validates a real folder: exists, is a dir, readable, counts files, no .git', async () => {
|
|
57
|
+
fs.writeFileSync(path.join(dir, 'a.ts'), '');
|
|
58
|
+
fs.writeFileSync(path.join(dir, 'b.ts'), '');
|
|
59
|
+
fs.mkdirSync(path.join(dir, 'sub'));
|
|
60
|
+
fs.writeFileSync(path.join(dir, 'sub', 'c.ts'), '');
|
|
61
|
+
const result = await (0, localPathValidation_1.validateLocalPath)(dir, (0, localPathValidationFs_1.realLocalPathDeps)());
|
|
62
|
+
expect(result).toEqual({ ok: true, name: path.basename(dir), fileCount: 3, isGitRepo: false });
|
|
63
|
+
});
|
|
64
|
+
it('detects a git repo via .git directory', async () => {
|
|
65
|
+
fs.mkdirSync(path.join(dir, '.git'));
|
|
66
|
+
const result = await (0, localPathValidation_1.validateLocalPath)(dir, (0, localPathValidationFs_1.realLocalPathDeps)());
|
|
67
|
+
expect(result.ok && result.isGitRepo).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
it('reports not-exists for a missing path', async () => {
|
|
70
|
+
const result = await (0, localPathValidation_1.validateLocalPath)(path.join(dir, 'does-not-exist'), (0, localPathValidationFs_1.realLocalPathDeps)());
|
|
71
|
+
expect(result).toEqual({ ok: false, error: 'not-exists' });
|
|
72
|
+
});
|
|
73
|
+
it('reports not-dir when the path is a plain file', async () => {
|
|
74
|
+
const filePath = path.join(dir, 'file.txt');
|
|
75
|
+
fs.writeFileSync(filePath, 'x');
|
|
76
|
+
const result = await (0, localPathValidation_1.validateLocalPath)(filePath, (0, localPathValidationFs_1.realLocalPathDeps)());
|
|
77
|
+
expect(result).toEqual({ ok: false, error: 'not-dir' });
|
|
78
|
+
});
|
|
79
|
+
it('skips node_modules and .git when counting files', async () => {
|
|
80
|
+
fs.mkdirSync(path.join(dir, 'node_modules'));
|
|
81
|
+
fs.writeFileSync(path.join(dir, 'node_modules', 'huge.js'), '');
|
|
82
|
+
fs.mkdirSync(path.join(dir, '.git'));
|
|
83
|
+
fs.writeFileSync(path.join(dir, '.git', 'HEAD'), '');
|
|
84
|
+
fs.writeFileSync(path.join(dir, 'index.ts'), '');
|
|
85
|
+
const result = await (0, localPathValidation_1.validateLocalPath)(dir, (0, localPathValidationFs_1.realLocalPathDeps)());
|
|
86
|
+
expect(result).toEqual({ ok: true, name: path.basename(dir), fileCount: 1, isGitRepo: true });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* TDD — lws-05: POST /local/validate-path support. Pure validation logic
|
|
5
|
+
* (fs injected — no real disk I/O in tests), backing the create-modal's
|
|
6
|
+
* typed-path fallback (PRD D3c): "my-app — 214 files, git repo detected".
|
|
7
|
+
*/
|
|
8
|
+
const localPathValidation_1 = require("../bridge/localPathValidation");
|
|
9
|
+
function deps(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
exists: () => true,
|
|
12
|
+
isDirectory: () => true,
|
|
13
|
+
readable: () => true,
|
|
14
|
+
countFiles: () => 3,
|
|
15
|
+
isGitRepo: () => false,
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
describe('validateLocalPath', () => {
|
|
20
|
+
it('returns ok:false not-exists when the path does not exist', async () => {
|
|
21
|
+
const result = await (0, localPathValidation_1.validateLocalPath)('/nope', deps({ exists: () => false }));
|
|
22
|
+
expect(result).toEqual({ ok: false, error: 'not-exists' });
|
|
23
|
+
});
|
|
24
|
+
it('returns ok:false not-dir when the path is a file, not a folder', async () => {
|
|
25
|
+
const result = await (0, localPathValidation_1.validateLocalPath)('/some/file.txt', deps({ isDirectory: () => false }));
|
|
26
|
+
expect(result).toEqual({ ok: false, error: 'not-dir' });
|
|
27
|
+
});
|
|
28
|
+
it('returns ok:false unreadable when the folder cannot be read', async () => {
|
|
29
|
+
const result = await (0, localPathValidation_1.validateLocalPath)('/locked', deps({ readable: () => false }));
|
|
30
|
+
expect(result).toEqual({ ok: false, error: 'unreadable' });
|
|
31
|
+
});
|
|
32
|
+
it('returns ok:true with name (basename), file count, and git-repo flag', async () => {
|
|
33
|
+
const result = await (0, localPathValidation_1.validateLocalPath)('/Users/x/my-app', deps({ countFiles: () => 214, isGitRepo: () => true }));
|
|
34
|
+
expect(result).toEqual({ ok: true, name: 'my-app', fileCount: 214, isGitRepo: true });
|
|
35
|
+
});
|
|
36
|
+
it('reports isGitRepo:false for a plain folder-of-code with no .git', async () => {
|
|
37
|
+
const result = await (0, localPathValidation_1.validateLocalPath)('/Users/x/scratch', deps({ isGitRepo: () => false }));
|
|
38
|
+
expect(result.ok && result.isGitRepo).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it('checks existence before directory-ness before readability (short-circuits, does not call later checks)', async () => {
|
|
41
|
+
const isDirectory = jest.fn(() => true);
|
|
42
|
+
await (0, localPathValidation_1.validateLocalPath)('/nope', deps({ exists: () => false, isDirectory }));
|
|
43
|
+
expect(isDirectory).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* pucc-14 — in-box login-validity check (ADR-0028 boundary).
|
|
5
|
+
*
|
|
6
|
+
* The bridge reduces the on-volume Claude Code login to a single BOOLEAN so the
|
|
7
|
+
* platform can render connected / needs_relogin without ever handling the
|
|
8
|
+
* credential. These tests pin: valid / expired / missing → boolean, and that
|
|
9
|
+
* the token value is NEVER extracted or returned — only the expiry is read.
|
|
10
|
+
*/
|
|
11
|
+
const loginStatus_1 = require("../bridge/loginStatus");
|
|
12
|
+
const CLAUDE_DIR = '/data/.claude';
|
|
13
|
+
const CRED_PATH = `${CLAUDE_DIR}/${loginStatus_1.CLAUDE_CREDENTIALS_FILE}`;
|
|
14
|
+
const NOW = 1_000_000_000_000;
|
|
15
|
+
const SECRET = 'sk-ant-oat-SUPER-SECRET-TOKEN-must-never-leak';
|
|
16
|
+
function makeDeps(over) {
|
|
17
|
+
const { file, missing, ...rest } = over;
|
|
18
|
+
return {
|
|
19
|
+
claudeDir: CLAUDE_DIR,
|
|
20
|
+
exists: (p) => !missing && p === CRED_PATH,
|
|
21
|
+
readJson: () => file,
|
|
22
|
+
now: () => NOW,
|
|
23
|
+
...rest,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
describe('checkClaudeLoginValid — existence + expiry only → boolean', () => {
|
|
27
|
+
it('returns false when the credentials file is missing (never logged in)', () => {
|
|
28
|
+
expect((0, loginStatus_1.checkClaudeLoginValid)(makeDeps({ missing: true }))).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it('returns true for a non-expired login (expiry in the future)', () => {
|
|
31
|
+
const file = { claudeAiOauth: { accessToken: SECRET, refreshToken: SECRET, expiresAt: NOW + 60_000 } };
|
|
32
|
+
expect((0, loginStatus_1.checkClaudeLoginValid)(makeDeps({ file }))).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
it('returns false for an expired login (expiry in the past)', () => {
|
|
35
|
+
const file = { claudeAiOauth: { accessToken: SECRET, expiresAt: NOW - 1 } };
|
|
36
|
+
expect((0, loginStatus_1.checkClaudeLoginValid)(makeDeps({ file }))).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
it('treats a present file with no expiry field as valid (existence is the signal)', () => {
|
|
39
|
+
const file = { claudeAiOauth: { accessToken: SECRET } };
|
|
40
|
+
expect((0, loginStatus_1.checkClaudeLoginValid)(makeDeps({ file }))).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
it('tolerates a flat (un-nested) shape', () => {
|
|
43
|
+
expect((0, loginStatus_1.checkClaudeLoginValid)(makeDeps({ file: { expiresAt: NOW + 5 } }))).toBe(true);
|
|
44
|
+
expect((0, loginStatus_1.checkClaudeLoginValid)(makeDeps({ file: { expiresAt: NOW - 5 } }))).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
it('returns false when the file is corrupt / unparseable', () => {
|
|
47
|
+
const deps = makeDeps({});
|
|
48
|
+
deps.readJson = () => {
|
|
49
|
+
throw new Error('unexpected token');
|
|
50
|
+
};
|
|
51
|
+
expect((0, loginStatus_1.checkClaudeLoginValid)(deps)).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
it('always returns a boolean and NEVER the token value', () => {
|
|
54
|
+
const file = { claudeAiOauth: { accessToken: SECRET, refreshToken: SECRET, expiresAt: NOW + 1 } };
|
|
55
|
+
const result = (0, loginStatus_1.checkClaudeLoginValid)(makeDeps({ file }));
|
|
56
|
+
expect(typeof result).toBe('boolean');
|
|
57
|
+
// The return type is boolean, so the token can never ride out through it.
|
|
58
|
+
expect(JSON.stringify(result)).not.toContain(SECRET);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ws_1 = require("ws");
|
|
4
|
+
const relayClient_1 = require("../bridge/relayClient");
|
|
5
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
6
|
+
// ── Fake PTY (no real shells in CI) ──────────────────────────────────
|
|
7
|
+
class FakePty {
|
|
8
|
+
written = [];
|
|
9
|
+
resizes = [];
|
|
10
|
+
killed = false;
|
|
11
|
+
dataCb = null;
|
|
12
|
+
write(data) { this.written.push(data); }
|
|
13
|
+
resize(cols, rows) { this.resizes.push({ cols, rows }); }
|
|
14
|
+
kill() { this.killed = true; }
|
|
15
|
+
onData(cb) { this.dataCb = cb; }
|
|
16
|
+
onExit(_cb) { }
|
|
17
|
+
emitData(d) { this.dataCb?.(d); }
|
|
18
|
+
}
|
|
19
|
+
const makeFactory = () => {
|
|
20
|
+
const ptys = [];
|
|
21
|
+
const factory = {
|
|
22
|
+
spawn: () => {
|
|
23
|
+
const p = new FakePty();
|
|
24
|
+
ptys.push(p);
|
|
25
|
+
return p;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
return { factory, ptys };
|
|
29
|
+
};
|
|
30
|
+
/** Collects parsed frames and lets tests await the next one (mirrors bridge-server.test.ts). */
|
|
31
|
+
function frameQueue(ws) {
|
|
32
|
+
const frames = [];
|
|
33
|
+
const waiters = [];
|
|
34
|
+
ws.on('message', (raw) => {
|
|
35
|
+
const f = JSON.parse(String(raw));
|
|
36
|
+
const w = waiters.shift();
|
|
37
|
+
if (w)
|
|
38
|
+
w(f);
|
|
39
|
+
else
|
|
40
|
+
frames.push(f);
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
next: () => frames.length > 0
|
|
44
|
+
? Promise.resolve(frames.shift())
|
|
45
|
+
: new Promise((resolve) => waiters.push(resolve)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** A loopback ws server standing in for the platform relay. */
|
|
49
|
+
function startFakeRelay() {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const wss = new ws_1.WebSocketServer({ port: 0, host: '127.0.0.1' });
|
|
52
|
+
const queue = [];
|
|
53
|
+
const waiters = [];
|
|
54
|
+
wss.on('connection', (ws) => {
|
|
55
|
+
const w = waiters.shift();
|
|
56
|
+
if (w)
|
|
57
|
+
w(ws);
|
|
58
|
+
else
|
|
59
|
+
queue.push(ws);
|
|
60
|
+
});
|
|
61
|
+
wss.on('listening', () => {
|
|
62
|
+
const addr = wss.address();
|
|
63
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
64
|
+
resolve({
|
|
65
|
+
wss,
|
|
66
|
+
port,
|
|
67
|
+
nextConnection: () => queue.length > 0 ? Promise.resolve(queue.shift()) : new Promise((r) => waiters.push(r)),
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function closeRelay(wss) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
for (const client of wss.clients)
|
|
75
|
+
client.terminate();
|
|
76
|
+
wss.close((err) => (err ? reject(err) : resolve()));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
describe('relay client — dial-out handshake', () => {
|
|
80
|
+
it('connects to the relay URL and sends the v1 auth frame first', async () => {
|
|
81
|
+
const relay = await startFakeRelay();
|
|
82
|
+
const { factory } = makeFactory();
|
|
83
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
84
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
85
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
86
|
+
relayToken: 'relay-token-abc',
|
|
87
|
+
sessions,
|
|
88
|
+
});
|
|
89
|
+
const serverSideWs = await relay.nextConnection();
|
|
90
|
+
const q = frameQueue(serverSideWs);
|
|
91
|
+
const authFrame = await q.next();
|
|
92
|
+
expect(authFrame).toEqual({ v: 1, relayToken: 'relay-token-abc' });
|
|
93
|
+
handle.close();
|
|
94
|
+
await closeRelay(relay.wss);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('relay client — frame service through the SessionManager', () => {
|
|
98
|
+
it('open -> opened; input reaches the PTY; PTY data streams back over the single socket', async () => {
|
|
99
|
+
const relay = await startFakeRelay();
|
|
100
|
+
const { factory, ptys } = makeFactory();
|
|
101
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
102
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
103
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
104
|
+
relayToken: 'tok',
|
|
105
|
+
sessions,
|
|
106
|
+
});
|
|
107
|
+
const ws = await relay.nextConnection();
|
|
108
|
+
const q = frameQueue(ws);
|
|
109
|
+
await q.next(); // auth frame
|
|
110
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
|
|
111
|
+
const opened = await q.next();
|
|
112
|
+
expect(opened.t).toBe('opened');
|
|
113
|
+
expect(opened.sessionId).toBeTruthy();
|
|
114
|
+
ws.send(JSON.stringify({ t: 'input', sessionId: opened.sessionId, data: 'ls\r' }));
|
|
115
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
116
|
+
expect(ptys[0].written).toEqual(['ls\r']);
|
|
117
|
+
ptys[0].emitData('result');
|
|
118
|
+
const data = await q.next();
|
|
119
|
+
expect(data).toMatchObject({ t: 'data', sessionId: opened.sessionId, data: 'result' });
|
|
120
|
+
handle.close();
|
|
121
|
+
await closeRelay(relay.wss);
|
|
122
|
+
});
|
|
123
|
+
it('second attach takes over: prior sink gets takeover, new one gets replay', async () => {
|
|
124
|
+
const relay = await startFakeRelay();
|
|
125
|
+
const { factory, ptys } = makeFactory();
|
|
126
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
127
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
128
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
129
|
+
relayToken: 'tok',
|
|
130
|
+
sessions,
|
|
131
|
+
});
|
|
132
|
+
const ws = await relay.nextConnection();
|
|
133
|
+
const q = frameQueue(ws);
|
|
134
|
+
await q.next(); // auth
|
|
135
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
|
|
136
|
+
const opened = await q.next();
|
|
137
|
+
ptys[0].emitData('history');
|
|
138
|
+
await q.next(); // 'history' data frame
|
|
139
|
+
ws.send(JSON.stringify({ t: 'attach', sessionId: opened.sessionId }));
|
|
140
|
+
const [takeover, replay] = [await q.next(), await q.next()];
|
|
141
|
+
// Order isn't guaranteed between takeover-to-old-sink and replay-to-new
|
|
142
|
+
// sink over the SAME socket, so match by type.
|
|
143
|
+
const byType = (t) => [takeover, replay].find((f) => f.t === t);
|
|
144
|
+
expect(byType('takeover')).toMatchObject({ t: 'takeover', sessionId: opened.sessionId });
|
|
145
|
+
expect(byType('replay')).toMatchObject({ t: 'replay', sessionId: opened.sessionId, data: 'history' });
|
|
146
|
+
handle.close();
|
|
147
|
+
await closeRelay(relay.wss);
|
|
148
|
+
});
|
|
149
|
+
it('multiplexes multiple concurrent sessions over the one outbound socket', async () => {
|
|
150
|
+
const relay = await startFakeRelay();
|
|
151
|
+
const { factory, ptys } = makeFactory();
|
|
152
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
153
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
154
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
155
|
+
relayToken: 'tok',
|
|
156
|
+
sessions,
|
|
157
|
+
});
|
|
158
|
+
const ws = await relay.nextConnection();
|
|
159
|
+
const q = frameQueue(ws);
|
|
160
|
+
await q.next(); // auth
|
|
161
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
|
|
162
|
+
const openedA = await q.next();
|
|
163
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
|
|
164
|
+
const openedB = await q.next();
|
|
165
|
+
expect(openedA.sessionId).not.toBe(openedB.sessionId);
|
|
166
|
+
ws.send(JSON.stringify({ t: 'input', sessionId: openedA.sessionId, data: 'to-a' }));
|
|
167
|
+
ws.send(JSON.stringify({ t: 'input', sessionId: openedB.sessionId, data: 'to-b' }));
|
|
168
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
169
|
+
expect(ptys[0].written).toEqual(['to-a']);
|
|
170
|
+
expect(ptys[1].written).toEqual(['to-b']);
|
|
171
|
+
ws.send(JSON.stringify({ t: 'resize', sessionId: openedB.sessionId, cols: 100, rows: 40 }));
|
|
172
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
173
|
+
expect(ptys[1].resizes).toEqual([{ cols: 100, rows: 40 }]);
|
|
174
|
+
ws.send(JSON.stringify({ t: 'close', sessionId: openedA.sessionId }));
|
|
175
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
176
|
+
expect(ptys[0].killed).toBe(true);
|
|
177
|
+
expect(sessions.list()).toEqual([openedB.sessionId]);
|
|
178
|
+
handle.close();
|
|
179
|
+
await closeRelay(relay.wss);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe('relay client — reconnect with backoff + jitter', () => {
|
|
183
|
+
it('on drop, schedules a reconnect using the computed backoff delay (injectable timer)', async () => {
|
|
184
|
+
const relay = await startFakeRelay();
|
|
185
|
+
const { factory } = makeFactory();
|
|
186
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
187
|
+
const scheduled = [];
|
|
188
|
+
const setTimeoutFn = (cb, ms) => {
|
|
189
|
+
scheduled.push({ cb, ms });
|
|
190
|
+
return scheduled.length; // opaque handle
|
|
191
|
+
};
|
|
192
|
+
const clearTimeoutFn = () => { };
|
|
193
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
194
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
195
|
+
relayToken: 'tok',
|
|
196
|
+
sessions,
|
|
197
|
+
backoff: { initialMs: 100, maxMs: 1000, factor: 2, jitter: () => 1 }, // deterministic: full ceiling
|
|
198
|
+
setTimeoutFn,
|
|
199
|
+
clearTimeoutFn,
|
|
200
|
+
});
|
|
201
|
+
const ws = await relay.nextConnection(); // already open by the time 'connection' fires
|
|
202
|
+
// Drop the connection from the relay side.
|
|
203
|
+
ws.terminate();
|
|
204
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
205
|
+
expect(scheduled.length).toBe(1);
|
|
206
|
+
expect(scheduled[0].ms).toBe(100); // attempt 0: 100 * 2^0 * jitter(1) = 100
|
|
207
|
+
handle.close();
|
|
208
|
+
await closeRelay(relay.wss);
|
|
209
|
+
});
|
|
210
|
+
it('backoff grows exponentially per attempt, capped at maxMs', async () => {
|
|
211
|
+
// A relay that never accepts a connection (nothing listens on this port)
|
|
212
|
+
// means every attempt fails before 'open' — so attempt count never
|
|
213
|
+
// resets, and we can observe pure exponential growth without racing a
|
|
214
|
+
// real handshake's success-resets-backoff behavior.
|
|
215
|
+
const probe = await startFakeRelay();
|
|
216
|
+
const deadPort = probe.port;
|
|
217
|
+
await closeRelay(probe.wss);
|
|
218
|
+
const { factory } = makeFactory();
|
|
219
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
220
|
+
const scheduled = [];
|
|
221
|
+
const setTimeoutFn = (cb, ms) => {
|
|
222
|
+
scheduled.push({ cb, ms });
|
|
223
|
+
return scheduled.length;
|
|
224
|
+
};
|
|
225
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
226
|
+
relayUrl: `ws://127.0.0.1:${deadPort}`,
|
|
227
|
+
relayToken: 'tok',
|
|
228
|
+
sessions,
|
|
229
|
+
backoff: { initialMs: 100, maxMs: 350, factor: 2, jitter: () => 1 },
|
|
230
|
+
setTimeoutFn,
|
|
231
|
+
clearTimeoutFn: () => { },
|
|
232
|
+
});
|
|
233
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
234
|
+
expect(scheduled[0]?.ms).toBe(100); // 100 * 2^0
|
|
235
|
+
scheduled[0].cb();
|
|
236
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
237
|
+
expect(scheduled[1]?.ms).toBe(200); // 100 * 2^1
|
|
238
|
+
scheduled[1].cb();
|
|
239
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
240
|
+
expect(scheduled[2]?.ms).toBe(350); // 100 * 2^2 = 400, capped at 350
|
|
241
|
+
handle.close();
|
|
242
|
+
});
|
|
243
|
+
it('reconnects and re-pairs: PTY survives the drop, replay reflects everything that happened while down', async () => {
|
|
244
|
+
const relay = await startFakeRelay();
|
|
245
|
+
const { factory, ptys } = makeFactory();
|
|
246
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
247
|
+
const scheduled = [];
|
|
248
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
249
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
250
|
+
relayToken: 'tok',
|
|
251
|
+
sessions,
|
|
252
|
+
backoff: { initialMs: 1, jitter: () => 0.01 },
|
|
253
|
+
setTimeoutFn: (cb) => {
|
|
254
|
+
scheduled.push(cb);
|
|
255
|
+
return scheduled.length;
|
|
256
|
+
},
|
|
257
|
+
clearTimeoutFn: () => { },
|
|
258
|
+
});
|
|
259
|
+
const ws1 = await relay.nextConnection();
|
|
260
|
+
const q1 = frameQueue(ws1);
|
|
261
|
+
await q1.next(); // auth
|
|
262
|
+
ws1.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
|
|
263
|
+
const opened = await q1.next();
|
|
264
|
+
// Drop the relay connection — the PTY must NOT be killed.
|
|
265
|
+
ws1.terminate();
|
|
266
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
267
|
+
expect(ptys[0].killed).toBe(false);
|
|
268
|
+
expect(sessions.list()).toEqual([opened.sessionId]);
|
|
269
|
+
// Session produces output while the relay link is down.
|
|
270
|
+
ptys[0].emitData('while-down');
|
|
271
|
+
// Fire the scheduled reconnect (simulates backoff timer elapsing).
|
|
272
|
+
expect(scheduled.length).toBe(1);
|
|
273
|
+
scheduled[0]();
|
|
274
|
+
const ws2 = await relay.nextConnection();
|
|
275
|
+
const q2 = frameQueue(ws2);
|
|
276
|
+
await q2.next(); // fresh auth frame on the new socket
|
|
277
|
+
ws2.send(JSON.stringify({ t: 'attach', sessionId: opened.sessionId }));
|
|
278
|
+
const replay = await q2.next();
|
|
279
|
+
expect(replay).toMatchObject({ t: 'replay', sessionId: opened.sessionId, data: 'while-down' });
|
|
280
|
+
handle.close();
|
|
281
|
+
await closeRelay(relay.wss);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const bridge_1 = require("../commands/bridge");
|
|
4
|
+
describe('resolveRelayMode — CLI/env wiring for `wen-market bridge --relay`', () => {
|
|
5
|
+
it('no --relay flag and no RELAY_URL env → local mode', () => {
|
|
6
|
+
expect((0, bridge_1.resolveRelayMode)({}, {})).toEqual({ mode: 'local' });
|
|
7
|
+
});
|
|
8
|
+
it('--relay without --relay-token → a clear startup error, no env fallback', () => {
|
|
9
|
+
const result = (0, bridge_1.resolveRelayMode)({ relay: 'wss://relay.hiveop.io/agent' }, {});
|
|
10
|
+
expect(result.mode).toBe('relay-error');
|
|
11
|
+
expect(result.message).toMatch(/relay-token/i);
|
|
12
|
+
});
|
|
13
|
+
it('--relay with --relay-token → relay mode with both values', () => {
|
|
14
|
+
const result = (0, bridge_1.resolveRelayMode)({ relay: 'wss://relay.hiveop.io/agent', relayToken: 'rt_abc123' }, {});
|
|
15
|
+
expect(result).toEqual({
|
|
16
|
+
mode: 'relay',
|
|
17
|
+
relayUrl: 'wss://relay.hiveop.io/agent',
|
|
18
|
+
relayToken: 'rt_abc123',
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it('RELAY_URL + RELAY_TOKEN env vars (container-friendly) → relay mode, no flags needed', () => {
|
|
22
|
+
const result = (0, bridge_1.resolveRelayMode)({}, { RELAY_URL: 'wss://relay.internal/x', RELAY_TOKEN: 'rt_env' });
|
|
23
|
+
expect(result).toEqual({ mode: 'relay', relayUrl: 'wss://relay.internal/x', relayToken: 'rt_env' });
|
|
24
|
+
});
|
|
25
|
+
it('RELAY_URL env with no token anywhere → clear startup error', () => {
|
|
26
|
+
const result = (0, bridge_1.resolveRelayMode)({}, { RELAY_URL: 'wss://relay.internal/x' });
|
|
27
|
+
expect(result.mode).toBe('relay-error');
|
|
28
|
+
});
|
|
29
|
+
it('flag values win over env vars when both present', () => {
|
|
30
|
+
const result = (0, bridge_1.resolveRelayMode)({ relay: 'wss://flag-wins/', relayToken: 'flag-token' }, { RELAY_URL: 'wss://env-loses/', RELAY_TOKEN: 'env-token' });
|
|
31
|
+
expect(result).toEqual({ mode: 'relay', relayUrl: 'wss://flag-wins/', relayToken: 'flag-token' });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|