@orboto/mcp 0.224.0 → 0.225.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/required-rules-wrapper.test.js +2 -1
- package/dist/test/child-window-test-timeout.test.js +20 -0
- package/dist/test/ci-window.js +8 -0
- package/dist/test/hermetic-cli-env.js +42 -0
- package/dist/tools/doc-replace-wrapper.test.js +5 -3
- package/dist/tools/review-policy-wrapper.test.js +5 -3
- package/dist/tools/semantic-degraded-wrapper.test.js +5 -3
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { spawn } from 'node:child_process';
|
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { dirname, join, resolve } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { hermeticHome, hermeticSpawnEnv } from './test/hermetic-cli-env.js';
|
|
8
9
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
9
10
|
const cleanup = [];
|
|
10
11
|
afterEach(async () => { for (const fn of cleanup.splice(0).reverse())
|
|
@@ -43,7 +44,7 @@ async function fixture() {
|
|
|
43
44
|
await writeFile(guard, `const original = globalThis.fetch; globalThis.fetch = (input, init) => { if (new URL(input).origin !== ${JSON.stringify(origin)}) throw new Error('Fixture refused a nonlocal request'); return original(input, init); };`);
|
|
44
45
|
const run = (args = ['session-start', '--force-rules']) => new Promise((ok, no) => {
|
|
45
46
|
const child = spawn(process.execPath, ['--import', guard, join(dir, 'skills/orboto/scripts/orboto.mjs'), ...args], {
|
|
46
|
-
cwd: dir, env: {
|
|
47
|
+
cwd: dir, env: hermeticSpawnEnv({ home: hermeticHome('mcp-wrapper'), baseUrl: `http://127.0.0.1:${address.port}`, token: 'orb_fixture', env: { ORBOTO_DAEMON: '', ORBOTO_AGENT_SESSION: 'fixture' } }),
|
|
47
48
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
48
49
|
});
|
|
49
50
|
let out = '', err = '';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { expect, it } from 'vitest';
|
|
2
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const src = join(process.cwd(), 'src');
|
|
5
|
+
function* testFiles(dir) {
|
|
6
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
7
|
+
const full = join(dir, entry.name);
|
|
8
|
+
if (entry.isDirectory())
|
|
9
|
+
yield* testFiles(full);
|
|
10
|
+
else if (entry.name.endsWith('.test.ts') && entry.name !== 'child-window-test-timeout.test.ts')
|
|
11
|
+
yield full;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
it('every mcp test using childWindow() sets its own ciWindow test timeout above it', () => {
|
|
15
|
+
const offenders = [...testFiles(src)].filter((file) => {
|
|
16
|
+
const text = readFileSync(file, 'utf8');
|
|
17
|
+
return text.includes('childWindow()') && !/\}, ciWindow\(\d[\d_]*\)\);/.test(text);
|
|
18
|
+
});
|
|
19
|
+
expect(offenders).toEqual([]);
|
|
20
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** ORB-2231 - a wait window that grows fivefold on the loaded shared CI runner; mirrors apps/api/src/test/ci-window.ts and imports no vitest on purpose. */
|
|
2
|
+
export function ciWindow(ms) {
|
|
3
|
+
return process.env.CI ? ms * 5 : ms;
|
|
4
|
+
}
|
|
5
|
+
/** ORB-2587 - the hang guard of one child process: 30 s, 5x on CI; the mcp runner timeout is 30 s unscaled, so a test using it sets its own ciWindow(...) test timeout. */
|
|
6
|
+
export function childWindow() {
|
|
7
|
+
return ciWindow(30_000);
|
|
8
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/** ORB-2590 - the environment of every test that spawns the Go CLI or the skill wrapper: an allowlist, never process.env, so a developer's ~/.orboto bindings, ORBOTO_* and the parent harness's CLAUDE* markers cannot reach the child. */
|
|
5
|
+
/** Runtime essentials and settings that do not identify the developer: the CLI reads CI (browser timeouts), a child may need the proxy, certificate and locale variables to reach the network. */
|
|
6
|
+
export const INHERITED_NAMES = [
|
|
7
|
+
'PATH', 'SystemRoot', 'SYSTEMROOT', 'WINDIR', 'ComSpec', 'PATHEXT', 'CI',
|
|
8
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
|
|
9
|
+
'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'LANG', 'LC_ALL', 'TZ',
|
|
10
|
+
];
|
|
11
|
+
const homes = new Set();
|
|
12
|
+
/** A fresh empty HOME for one test; removed when the test process exits. */
|
|
13
|
+
export function hermeticHome(label = 'spawn') {
|
|
14
|
+
const dir = mkdtempSync(join(tmpdir(), `orboto-hermetic-${label}-`));
|
|
15
|
+
homes.add(dir);
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
process.once('exit', () => { for (const dir of homes) {
|
|
19
|
+
try {
|
|
20
|
+
rmSync(dir, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
catch { /* best effort */ }
|
|
23
|
+
} });
|
|
24
|
+
/** HOME, USERPROFILE, XDG and temp dirs all inside `home`, the runtime essentials, an explicit instance, and only the `env` entries the test names. */
|
|
25
|
+
export function hermeticSpawnEnv({ home, baseUrl, token, profile, env = {} }, inherited = process.env) {
|
|
26
|
+
const out = {
|
|
27
|
+
HOME: home, USERPROFILE: home, TMPDIR: home, TMP: home, TEMP: home,
|
|
28
|
+
XDG_CONFIG_HOME: join(home, '.config'), XDG_STATE_HOME: join(home, '.local', 'state'), XDG_CACHE_HOME: join(home, '.cache'),
|
|
29
|
+
};
|
|
30
|
+
for (const name of INHERITED_NAMES) {
|
|
31
|
+
const value = inherited[name];
|
|
32
|
+
if (value)
|
|
33
|
+
out[name] = value;
|
|
34
|
+
}
|
|
35
|
+
if (baseUrl !== undefined)
|
|
36
|
+
out.ORBOTO_BASE_URL = baseUrl;
|
|
37
|
+
if (token !== undefined)
|
|
38
|
+
out.ORBOTO_TOKEN = token;
|
|
39
|
+
if (profile !== undefined)
|
|
40
|
+
out.ORBOTO_PROFILE = profile;
|
|
41
|
+
return { ...out, ...env };
|
|
42
|
+
}
|
|
@@ -9,6 +9,8 @@ import { createServer } from 'node:http';
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
|
+
import { childWindow, ciWindow } from '../test/ci-window.js';
|
|
13
|
+
import { hermeticHome, hermeticSpawnEnv } from '../test/hermetic-cli-env.js';
|
|
12
14
|
const DOC = 'd0c00000-2222-4222-8222-222222222222';
|
|
13
15
|
const REV = 'bbbbbbbb-0000-4000-8000-000000000000';
|
|
14
16
|
it('doc edit appends by default, --replace names the replaced revision, doc restore posts the restore', async () => {
|
|
@@ -45,8 +47,8 @@ it('doc edit appends by default, --replace names the replaced revision, doc rest
|
|
|
45
47
|
await writeFile(guard, `const original = globalThis.fetch; globalThis.fetch = (input, init) => { if (new URL(input).origin !== ${JSON.stringify(origin)}) throw new Error('Fixture refused nonlocal request'); return original(input, init); };`);
|
|
46
48
|
const run = (args) => new Promise((ok, no) => {
|
|
47
49
|
const child = spawn(process.execPath, ['--import', guard, join(dir, 'skills/orboto/scripts/orboto.mjs'), ...args], {
|
|
48
|
-
cwd: dir, env: {
|
|
49
|
-
timeout:
|
|
50
|
+
cwd: dir, env: hermeticSpawnEnv({ home: hermeticHome('mcp-wrapper'), baseUrl: origin, token: 'orb_fixture', env: { ORBOTO_DAEMON: '', ORBOTO_AGENT_SESSION: 'fixture' } }),
|
|
51
|
+
timeout: childWindow(),
|
|
50
52
|
});
|
|
51
53
|
let out = '', err = '';
|
|
52
54
|
child.stdout.on('data', (chunk) => { out += chunk.toString(); });
|
|
@@ -78,4 +80,4 @@ it('doc edit appends by default, --replace names the replaced revision, doc rest
|
|
|
78
80
|
await new Promise((ok, no) => server.close((err) => err ? no(err) : ok()));
|
|
79
81
|
await rm(dir, { recursive: true, force: true });
|
|
80
82
|
}
|
|
81
|
-
}, 60_000);
|
|
83
|
+
}, ciWindow(60_000));
|
|
@@ -4,6 +4,8 @@ import { createServer } from 'node:http';
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
+
import { childWindow, ciWindow } from '../test/ci-window.js';
|
|
8
|
+
import { hermeticHome, hermeticSpawnEnv } from '../test/hermetic-cli-env.js';
|
|
7
9
|
it('real wrapper preserves piped diff text and version-prefixed check/record values', async () => {
|
|
8
10
|
const dir = await mkdtemp(join(tmpdir(), 'orboto-1964-verify-temp-'));
|
|
9
11
|
const fingerprint = 'sha256-diff-v2:' + 'a'.repeat(64);
|
|
@@ -35,8 +37,8 @@ it('real wrapper preserves piped diff text and version-prefixed check/record val
|
|
|
35
37
|
await writeFile(guard, `const original = globalThis.fetch; globalThis.fetch = (input, init) => { if (new URL(input).origin !== ${JSON.stringify(origin)}) throw new Error('Fixture refused nonlocal request'); return original(input, init); };`);
|
|
36
38
|
const run = (args, stdin = '') => new Promise((ok, no) => {
|
|
37
39
|
const child = spawn(process.execPath, ['--import', guard, join(dir, 'skills/orboto/scripts/orboto.mjs'), ...args], {
|
|
38
|
-
cwd: dir, env: {
|
|
39
|
-
timeout:
|
|
40
|
+
cwd: dir, env: hermeticSpawnEnv({ home: hermeticHome('mcp-wrapper'), baseUrl: origin, token: 'orb_fixture', env: { ORBOTO_DAEMON: '', ORBOTO_AGENT_SESSION: 'fixture' } }),
|
|
41
|
+
timeout: childWindow(),
|
|
40
42
|
});
|
|
41
43
|
let out = '', err = '';
|
|
42
44
|
child.stdout.on('data', (chunk) => { out += chunk.toString(); });
|
|
@@ -59,4 +61,4 @@ it('real wrapper preserves piped diff text and version-prefixed check/record val
|
|
|
59
61
|
await new Promise((ok, no) => server.close((err) => err ? no(err) : ok()));
|
|
60
62
|
await rm(dir, { recursive: true, force: true });
|
|
61
63
|
}
|
|
62
|
-
}, 60_000);
|
|
64
|
+
}, ciWindow(60_000));
|
|
@@ -8,6 +8,8 @@ import { createServer } from 'node:http';
|
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
+
import { childWindow, ciWindow } from '../test/ci-window.js';
|
|
12
|
+
import { hermeticHome, hermeticSpawnEnv } from '../test/hermetic-cli-env.js';
|
|
11
13
|
const LINE = 'semantic search unavailable, showing full-text results';
|
|
12
14
|
it('search, search-docs, ask-docs and check-similar print the degraded line on stderr', async () => {
|
|
13
15
|
const dir = await mkdtemp(join(tmpdir(), 'orboto-2543-wrapper-'));
|
|
@@ -45,8 +47,8 @@ it('search, search-docs, ask-docs and check-similar print the degraded line on s
|
|
|
45
47
|
await writeFile(guard, `const original = globalThis.fetch; globalThis.fetch = (input, init) => { if (new URL(input).origin !== ${JSON.stringify(origin)}) throw new Error('Fixture refused nonlocal request'); return original(input, init); };`);
|
|
46
48
|
const run = (args) => new Promise((ok, no) => {
|
|
47
49
|
const child = spawn(process.execPath, ['--import', guard, join(dir, 'skills/orboto/scripts/orboto.mjs'), ...args], {
|
|
48
|
-
cwd: dir, env: {
|
|
49
|
-
timeout:
|
|
50
|
+
cwd: dir, env: hermeticSpawnEnv({ home: hermeticHome('mcp-wrapper'), baseUrl: origin, token: 'orb_fixture', env: { ORBOTO_DAEMON: '', ORBOTO_AGENT_SESSION: 'fixture' } }),
|
|
51
|
+
timeout: childWindow(),
|
|
50
52
|
});
|
|
51
53
|
let out = '', err = '';
|
|
52
54
|
child.stdout.on('data', (chunk) => { out += chunk.toString(); });
|
|
@@ -67,4 +69,4 @@ it('search, search-docs, ask-docs and check-similar print the degraded line on s
|
|
|
67
69
|
await new Promise((ok, no) => server.close((err) => err ? no(err) : ok()));
|
|
68
70
|
await rm(dir, { recursive: true, force: true });
|
|
69
71
|
}
|
|
70
|
-
}, 60_000);
|
|
72
|
+
}, ciWindow(60_000));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orboto/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.225.0",
|
|
4
4
|
"description": "Model Context Protocol server for orboto - exposes tickets, projects, milestones, time, docs, primer-facts, alerts and absences as MCP tools for Claude Desktop / Cursor / Copilot Chat / any MCP-aware AI client.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|