ultra-builder-pro-cli 0.5.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/.ultra-template/delivery-report.json +9 -0
- package/.ultra-template/docs/research/README.md +53 -0
- package/.ultra-template/specs/architecture.md +250 -0
- package/.ultra-template/specs/discovery.md +248 -0
- package/.ultra-template/specs/product.md +223 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +57 -0
- package/.ultra-template/tasks/tasks.json +33 -0
- package/.ultra-template/test-report.json +8 -0
- package/CHANGELOG.md +194 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/adapters/_shared/codex-assets.cjs +1128 -0
- package/adapters/_shared/codex-ultra-tools-entry.cjs +11 -0
- package/adapters/_shared/file-ops.cjs +115 -0
- package/adapters/_shared/frontmatter.cjs +52 -0
- package/adapters/_shared/handbook.cjs +144 -0
- package/adapters/_shared/md-to-toml.cjs +54 -0
- package/adapters/_shared/path-rewrite.cjs +79 -0
- package/adapters/_shared/rtk-detect.cjs +86 -0
- package/adapters/_shared/runtime-assets.cjs +70 -0
- package/adapters/_shared/settings-merge.cjs +76 -0
- package/adapters/_shared/tests/file-ops.test.cjs +54 -0
- package/adapters/_shared/tests/frontmatter.test.cjs +47 -0
- package/adapters/_shared/tests/handbook.test.cjs +86 -0
- package/adapters/_shared/tests/md-to-toml.test.cjs +48 -0
- package/adapters/_shared/tests/path-rewrite.test.cjs +60 -0
- package/adapters/_shared/tests/resolve-target.test.cjs +85 -0
- package/adapters/_shared/tests/rtk-detect.test.cjs +116 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +107 -0
- package/adapters/_shared/tests/settings-merge.test.cjs +85 -0
- package/adapters/_shared/tests/validate.test.cjs +30 -0
- package/adapters/_shared/validate.cjs +24 -0
- package/adapters/claude.js +182 -0
- package/adapters/codex.js +385 -0
- package/adapters/gemini.js +204 -0
- package/adapters/opencode.js +506 -0
- package/adapters/tests/claude.test.cjs +117 -0
- package/adapters/tests/codex-hook.test.cjs +90 -0
- package/adapters/tests/codex.test.cjs +356 -0
- package/adapters/tests/gemini.test.cjs +112 -0
- package/adapters/tests/opencode.test.cjs +246 -0
- package/agents/code-reviewer.md +102 -0
- package/agents/debugger.md +127 -0
- package/agents/review-code.md +125 -0
- package/agents/review-comments.md +98 -0
- package/agents/review-coordinator.md +146 -0
- package/agents/review-design.md +133 -0
- package/agents/review-errors.md +87 -0
- package/agents/review-tests.md +93 -0
- package/agents/tdd-runner.md +92 -0
- package/bin/handbook.js +54 -0
- package/bin/install.js +254 -0
- package/bin/orchestrator.js +214 -0
- package/commands/learn.md +41 -0
- package/commands/ultra-deliver.md +45 -0
- package/commands/ultra-dev.md +50 -0
- package/commands/ultra-init.md +51 -0
- package/commands/ultra-plan.md +49 -0
- package/commands/ultra-research.md +59 -0
- package/commands/ultra-status.md +46 -0
- package/commands/ultra-test.md +48 -0
- package/commands/ultra-think.md +45 -0
- package/docs/AGENT-CONTEXT.md +143 -0
- package/docs/ARCHITECTURE.md +197 -0
- package/docs/COMMIT-HASH-BACKFILL.md +101 -0
- package/docs/LEGACY-HERMES.md +1175 -0
- package/docs/PLAN.zh-CN.md +1683 -0
- package/docs/ROADMAP.md +102 -0
- package/docs/RUNTIME-COMPAT-MATRIX.md +98 -0
- package/docs/STATE-DB-ACCESS-POLICY.md +109 -0
- package/docs/USER-HANDBOOK-CONTRACT.md +61 -0
- package/hooks/README.md +24 -0
- package/hooks/active_task_context.py +44 -0
- package/hooks/adapters/codex.py +302 -0
- package/hooks/health_check.py +61 -0
- package/hooks/pre_stop_check.py +56 -0
- package/hooks/subagent_tracker.py +116 -0
- package/hooks/workflow_checkpoint.py +56 -0
- package/hooks/workflow_context.py +63 -0
- package/hooks/workflow_resume.py +50 -0
- package/mcp-server/lib/init-project.cjs +162 -0
- package/mcp-server/lib/llm-client.cjs +157 -0
- package/mcp-server/lib/llm-client.test.cjs +156 -0
- package/mcp-server/lib/plan-store.cjs +96 -0
- package/mcp-server/lib/plan-store.test.cjs +132 -0
- package/mcp-server/lib/prd-parser.cjs +110 -0
- package/mcp-server/lib/prd-parser.test.cjs +171 -0
- package/mcp-server/lib/pricing.cjs +53 -0
- package/mcp-server/lib/projector.cjs +158 -0
- package/mcp-server/lib/state-db.cjs +113 -0
- package/mcp-server/lib/state-db.test.cjs +116 -0
- package/mcp-server/lib/state-ops.cjs +725 -0
- package/mcp-server/lib/task-expander.cjs +123 -0
- package/mcp-server/lib/task-expander.test.cjs +253 -0
- package/mcp-server/lib/telemetry.cjs +72 -0
- package/mcp-server/lib/topo.cjs +131 -0
- package/mcp-server/lib/topo.test.cjs +158 -0
- package/mcp-server/server.cjs +482 -0
- package/mcp-server/tests/circuit-breaker.test.cjs +125 -0
- package/mcp-server/tests/commit-hash-flow.test.cjs +117 -0
- package/mcp-server/tests/concurrency-update.test.cjs +96 -0
- package/mcp-server/tests/concurrency.test.cjs +112 -0
- package/mcp-server/tests/db-ops.test.cjs +117 -0
- package/mcp-server/tests/init-project.test.cjs +141 -0
- package/mcp-server/tests/migrate.test.cjs +144 -0
- package/mcp-server/tests/projector.test.cjs +130 -0
- package/mcp-server/tests/server-contract.test.cjs +750 -0
- package/mcp-server/tests/staleness.test.cjs +190 -0
- package/mcp-server/tests/state-ops.test.cjs +286 -0
- package/mcp-server/tests/tagged-tasks.test.cjs +163 -0
- package/mcp-server/tests/telemetry.test.cjs +205 -0
- package/orchestrator/auto-merge.cjs +109 -0
- package/orchestrator/code-graph-watcher.cjs +121 -0
- package/orchestrator/daemon.cjs +112 -0
- package/orchestrator/dispatch-rules.cjs +93 -0
- package/orchestrator/parallel-orchestrator.cjs +195 -0
- package/orchestrator/planner/expand-prompt.cjs +62 -0
- package/orchestrator/planner/expand-prompt.test.cjs +52 -0
- package/orchestrator/planner/plan-builder.cjs +116 -0
- package/orchestrator/planner/plan-builder.test.cjs +165 -0
- package/orchestrator/planner/prd-prompt.cjs +44 -0
- package/orchestrator/planner/prd-prompt.test.cjs +30 -0
- package/orchestrator/recovery.cjs +70 -0
- package/orchestrator/session-runner.cjs +224 -0
- package/orchestrator/skill-miner.cjs +133 -0
- package/orchestrator/tests/auto-merge.test.cjs +216 -0
- package/orchestrator/tests/code-graph-watcher.test.cjs +183 -0
- package/orchestrator/tests/daemon.test.cjs +260 -0
- package/orchestrator/tests/dispatch-rules.test.cjs +122 -0
- package/orchestrator/tests/parallel-orchestrator.test.cjs +233 -0
- package/orchestrator/tests/phase-8b-gate.test.cjs +183 -0
- package/orchestrator/tests/recovery.test.cjs +190 -0
- package/orchestrator/tests/session-runner.test.cjs +314 -0
- package/orchestrator/tests/skill-miner.test.cjs +147 -0
- package/orchestrator/tests/worktree-manager.test.cjs +148 -0
- package/orchestrator/worktree-manager.cjs +131 -0
- package/output-styles/coding-vibes.md +67 -0
- package/output-styles/structural-thinking.md +61 -0
- package/package.json +109 -0
- package/settings.json +8 -0
- package/skills/cc-collab/SKILL.md +41 -0
- package/skills/code-review-expert/SKILL.md +118 -0
- package/skills/code-review-expert/references/code-quality-checklist.md +112 -0
- package/skills/code-review-expert/references/integration-checklist.md +46 -0
- package/skills/code-review-expert/references/removal-plan.md +52 -0
- package/skills/code-review-expert/references/security-checklist.md +100 -0
- package/skills/code-review-expert/references/solid-checklist.md +65 -0
- package/skills/codex-collab/SKILL.md +52 -0
- package/skills/codex-collab/evals/evals.json +40 -0
- package/skills/gemini-collab/SKILL.md +51 -0
- package/skills/gemini-collab/evals/evals.json +42 -0
- package/skills/integration-rules/SKILL.md +71 -0
- package/skills/learn/SKILL.md +117 -0
- package/skills/security-rules/SKILL.md +60 -0
- package/skills/testing-rules/SKILL.md +56 -0
- package/skills/ultra-deliver/SKILL.md +171 -0
- package/skills/ultra-dev/SKILL.md +324 -0
- package/skills/ultra-init/SKILL.md +148 -0
- package/skills/ultra-plan/SKILL.md +314 -0
- package/skills/ultra-research/SKILL.md +156 -0
- package/skills/ultra-research/steps/step-00-problem-validation.md +154 -0
- package/skills/ultra-research/steps/step-01-opportunity-discovery.md +172 -0
- package/skills/ultra-research/steps/step-02-market-assessment.md +195 -0
- package/skills/ultra-research/steps/step-03-competitive-landscape.md +197 -0
- package/skills/ultra-research/steps/step-04-product-strategy.md +196 -0
- package/skills/ultra-research/steps/step-05-assumptions-validation.md +234 -0
- package/skills/ultra-research/steps/step-10-user-personas.md +169 -0
- package/skills/ultra-research/steps/step-11-user-scenarios.md +181 -0
- package/skills/ultra-research/steps/step-20-user-stories.md +156 -0
- package/skills/ultra-research/steps/step-21-features-scope.md +157 -0
- package/skills/ultra-research/steps/step-22-success-metrics.md +195 -0
- package/skills/ultra-research/steps/step-30-architecture-context.md +154 -0
- package/skills/ultra-research/steps/step-31-solution-strategy.md +172 -0
- package/skills/ultra-research/steps/step-32-building-blocks.md +188 -0
- package/skills/ultra-research/steps/step-40-deployment.md +123 -0
- package/skills/ultra-research/steps/step-41-quality-risks.md +135 -0
- package/skills/ultra-research/steps/step-99-synthesis.md +188 -0
- package/skills/ultra-review/SKILL.md +437 -0
- package/skills/ultra-review/references/unified-schema.md +127 -0
- package/skills/ultra-review/scripts/review_verdict_update.py +99 -0
- package/skills/ultra-review/scripts/review_wait.py +120 -0
- package/skills/ultra-status/SKILL.md +162 -0
- package/skills/ultra-test/SKILL.md +195 -0
- package/skills/ultra-think/SKILL.md +109 -0
- package/skills/ultra-verify/SKILL.md +111 -0
- package/skills/ultra-verify/evals/evals.json +51 -0
- package/skills/ultra-verify/references/confidence-system.md +38 -0
- package/skills/ultra-verify/references/cross-verify-modes.md +73 -0
- package/skills/ultra-verify/scripts/verify_wait.py +131 -0
- package/spec/cli-protocol.md +118 -0
- package/spec/mcp-tools.yaml +658 -0
- package/spec/schemas/command-manifest.schema.json +47 -0
- package/spec/schemas/context-file.v4.5.schema.json +54 -0
- package/spec/schemas/mcp-tools.schema.json +136 -0
- package/spec/schemas/skill-manifest.schema.json +94 -0
- package/spec/schemas/state-db.sql +154 -0
- package/spec/schemas/tasks.v4.5.schema.json +66 -0
- package/ultra-tools/cli.cjs +121 -0
- package/ultra-tools/commands/db.cjs +175 -0
- package/ultra-tools/commands/legacy-memory.cjs +159 -0
- package/ultra-tools/commands/migrate.cjs +354 -0
- package/ultra-tools/commands/session.cjs +180 -0
- package/ultra-tools/commands/status.cjs +158 -0
- package/ultra-tools/commands/task.cjs +95 -0
- package/ultra-tools/legacy-memory.test.cjs +80 -0
- package/ultra-tools/status-command.test.cjs +132 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { spawnSync } = require('node:child_process');
|
|
9
|
+
|
|
10
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
11
|
+
const ADAPTER = path.join(REPO_ROOT, 'hooks', 'adapters', 'codex.py');
|
|
12
|
+
|
|
13
|
+
function run(feature, payload) {
|
|
14
|
+
return spawnSync('python3', [ADAPTER, feature], {
|
|
15
|
+
cwd: REPO_ROOT,
|
|
16
|
+
input: JSON.stringify(payload),
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
env: { ...process.env, PLUGIN_DATA: path.join(REPO_ROOT, '.tmp-codex-hook-test') },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('Codex hook adapter rejects retired generic policy hooks', () => {
|
|
23
|
+
const result = run('block_dangerous_commands.py', {
|
|
24
|
+
session_id: 'session-1',
|
|
25
|
+
turn_id: 'turn-1',
|
|
26
|
+
cwd: REPO_ROOT,
|
|
27
|
+
hook_event_name: 'PreToolUse',
|
|
28
|
+
tool_name: 'Bash',
|
|
29
|
+
tool_input: { command: 'git reset --hard' },
|
|
30
|
+
});
|
|
31
|
+
assert.equal(result.status, 0, result.stderr);
|
|
32
|
+
const output = JSON.parse(result.stdout);
|
|
33
|
+
assert.match(output.systemMessage, /refused unknown feature/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('Codex hook adapter injects only an active Ultra task boundary before edits', () => {
|
|
37
|
+
const project = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-codex-hook-'));
|
|
38
|
+
try {
|
|
39
|
+
fs.mkdirSync(path.join(project, '.ultra'));
|
|
40
|
+
fs.writeFileSync(path.join(project, '.ultra', 'workflow-state.json'), JSON.stringify({
|
|
41
|
+
command: 'ultra-dev', task_id: 'task-7', step: '4.5', status: 'review_pending',
|
|
42
|
+
}));
|
|
43
|
+
const result = run('active_task_context.py', {
|
|
44
|
+
session_id: 'session-2',
|
|
45
|
+
cwd: project,
|
|
46
|
+
hook_event_name: 'PreToolUse',
|
|
47
|
+
tool_name: 'apply_patch',
|
|
48
|
+
tool_input: { command: '*** Begin Patch\n*** Update File: src/a.py\n*** End Patch' },
|
|
49
|
+
});
|
|
50
|
+
assert.equal(result.status, 0, result.stderr);
|
|
51
|
+
const output = JSON.parse(result.stdout);
|
|
52
|
+
assert.equal(output.hookSpecificOutput.hookEventName, 'PreToolUse');
|
|
53
|
+
assert.match(output.hookSpecificOutput.additionalContext, /task-7/);
|
|
54
|
+
assert.doesNotMatch(JSON.stringify(output), /memory|recall|journal/);
|
|
55
|
+
} finally {
|
|
56
|
+
fs.rmSync(project, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('Codex hook adapter maps apply_patch payloads to Edit-compatible file inputs', () => {
|
|
61
|
+
const script = [
|
|
62
|
+
'import importlib.util, json, pathlib',
|
|
63
|
+
`p = pathlib.Path(${JSON.stringify(ADAPTER)})`,
|
|
64
|
+
's = importlib.util.spec_from_file_location("codex_hook_adapter", p)',
|
|
65
|
+
'm = importlib.util.module_from_spec(s)',
|
|
66
|
+
's.loader.exec_module(m)',
|
|
67
|
+
'payload = {"hook_event_name":"PostToolUse","tool_name":"apply_patch","tool_input":{"patch":"*** Begin Patch\\n*** Update File: src/a.py\\n*** Add File: src/b.py\\n*** End Patch"}}',
|
|
68
|
+
'print(json.dumps(m.normalize_inputs(payload)))',
|
|
69
|
+
].join('; ');
|
|
70
|
+
const result = spawnSync('python3', ['-c', script], { cwd: REPO_ROOT, encoding: 'utf8' });
|
|
71
|
+
assert.equal(result.status, 0, result.stderr);
|
|
72
|
+
const outputs = JSON.parse(result.stdout);
|
|
73
|
+
assert.deepEqual(outputs.map((entry) => entry.tool_input.file_path), ['src/a.py', 'src/b.py']);
|
|
74
|
+
assert.ok(outputs.every((entry) => entry.tool_name === 'Edit'));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('Codex hook adapter converts compact recovery context to PostCompact schema', () => {
|
|
78
|
+
const script = [
|
|
79
|
+
'import importlib.util, json, pathlib',
|
|
80
|
+
`p = pathlib.Path(${JSON.stringify(ADAPTER)})`,
|
|
81
|
+
's = importlib.util.spec_from_file_location("codex_hook_adapter", p)',
|
|
82
|
+
'm = importlib.util.module_from_spec(s)',
|
|
83
|
+
's.loader.exec_module(m)',
|
|
84
|
+
'legacy = {"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"restored"}}',
|
|
85
|
+
'print(json.dumps(m.adapt_output(legacy, "PostCompact")))',
|
|
86
|
+
].join('; ');
|
|
87
|
+
const result = spawnSync('python3', ['-c', script], { cwd: REPO_ROOT, encoding: 'utf8' });
|
|
88
|
+
assert.equal(result.status, 0, result.stderr);
|
|
89
|
+
assert.deepEqual(JSON.parse(result.stdout), { systemMessage: 'restored' });
|
|
90
|
+
});
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { spawnSync } = require('node:child_process');
|
|
9
|
+
const yaml = require('js-yaml');
|
|
10
|
+
const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
|
|
11
|
+
const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
|
|
12
|
+
|
|
13
|
+
const codex = require('../codex.js');
|
|
14
|
+
const { parse: parseFrontmatter } = require('../_shared/frontmatter.cjs');
|
|
15
|
+
const PACKAGE_VERSION = require('../../package.json').version;
|
|
16
|
+
const {
|
|
17
|
+
skillsForRuntime,
|
|
18
|
+
WORKFLOW_HOOK_FILES,
|
|
19
|
+
} = require('../_shared/runtime-assets.cjs');
|
|
20
|
+
|
|
21
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
22
|
+
const COMMANDS = [
|
|
23
|
+
'learn',
|
|
24
|
+
'ultra-deliver',
|
|
25
|
+
'ultra-dev',
|
|
26
|
+
'ultra-init',
|
|
27
|
+
'ultra-plan',
|
|
28
|
+
'ultra-research',
|
|
29
|
+
'ultra-status',
|
|
30
|
+
'ultra-test',
|
|
31
|
+
'ultra-think',
|
|
32
|
+
];
|
|
33
|
+
const AGENTS = [
|
|
34
|
+
'code-reviewer',
|
|
35
|
+
'debugger',
|
|
36
|
+
'review-code',
|
|
37
|
+
'review-comments',
|
|
38
|
+
'review-coordinator',
|
|
39
|
+
'review-design',
|
|
40
|
+
'review-errors',
|
|
41
|
+
'review-tests',
|
|
42
|
+
'tdd-runner',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function mkLayout() {
|
|
46
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-codex-home-'));
|
|
47
|
+
return {
|
|
48
|
+
homeDir,
|
|
49
|
+
configDir: path.join(homeDir, '.codex'),
|
|
50
|
+
pluginRoot: path.join(homeDir, 'plugins', 'ultra-builder-pro'),
|
|
51
|
+
marketplaceFile: path.join(homeDir, '.agents', 'plugins', 'marketplace.json'),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cleanup(layout) {
|
|
56
|
+
fs.rmSync(layout.homeDir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function install(layout) {
|
|
60
|
+
return codex.install({
|
|
61
|
+
configDir: layout.configDir,
|
|
62
|
+
homeDir: layout.homeDir,
|
|
63
|
+
scope: 'global',
|
|
64
|
+
repoRoot: REPO_ROOT,
|
|
65
|
+
runPluginCli: false,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function skillNames(pluginRoot) {
|
|
70
|
+
return fs.readdirSync(path.join(pluginRoot, 'skills'), { withFileTypes: true })
|
|
71
|
+
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(pluginRoot, 'skills', entry.name, 'SKILL.md')))
|
|
72
|
+
.map((entry) => entry.name)
|
|
73
|
+
.sort();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test('install builds one Codex-native plugin with complete skill and command coverage', () => {
|
|
77
|
+
const layout = mkLayout();
|
|
78
|
+
try {
|
|
79
|
+
const report = install(layout);
|
|
80
|
+
assert.equal(report.plugin.root, layout.pluginRoot);
|
|
81
|
+
|
|
82
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(layout.pluginRoot, '.codex-plugin', 'plugin.json'), 'utf8'));
|
|
83
|
+
assert.equal(manifest.name, 'ultra-builder-pro');
|
|
84
|
+
const escapedVersion = PACKAGE_VERSION.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
85
|
+
assert.match(manifest.version, new RegExp(`^${escapedVersion}\\+codex\\.[0-9a-f]{12}$`));
|
|
86
|
+
assert.equal(manifest.skills, './skills/');
|
|
87
|
+
assert.equal(manifest.mcpServers, './.mcp.json');
|
|
88
|
+
assert.ok(!Object.hasOwn(manifest, 'hooks'), 'default hooks/hooks.json discovery avoids unsupported manifest field');
|
|
89
|
+
|
|
90
|
+
const expectedSkills = skillsForRuntime('codex').sort();
|
|
91
|
+
assert.deepEqual(skillNames(layout.pluginRoot), expectedSkills);
|
|
92
|
+
assert.equal(expectedSkills.length, 17);
|
|
93
|
+
assert.ok(!fs.existsSync(path.join(layout.pluginRoot, 'skills', 'codex-collab')));
|
|
94
|
+
assert.ok(!fs.existsSync(path.join(layout.pluginRoot, 'skills', 'learned')));
|
|
95
|
+
|
|
96
|
+
const commandMap = JSON.parse(fs.readFileSync(path.join(layout.pluginRoot, 'command-map.json'), 'utf8'));
|
|
97
|
+
assert.deepEqual(Object.keys(commandMap).sort(), COMMANDS.map((name) => `/${name}`).sort());
|
|
98
|
+
for (const command of COMMANDS) {
|
|
99
|
+
assert.equal(commandMap[`/${command}`], `$ultra-builder-pro:${command}`);
|
|
100
|
+
assert.ok(fs.existsSync(path.join(layout.pluginRoot, 'skills', command, 'SKILL.md')));
|
|
101
|
+
}
|
|
102
|
+
assert.ok(!fs.existsSync(path.join(layout.configDir, 'prompts')), 'deprecated Codex prompts must not be installed');
|
|
103
|
+
} finally {
|
|
104
|
+
cleanup(layout);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('every generated skill is Codex-valid, UI-visible, and free of Claude host bindings', () => {
|
|
109
|
+
const layout = mkLayout();
|
|
110
|
+
try {
|
|
111
|
+
install(layout);
|
|
112
|
+
for (const name of skillNames(layout.pluginRoot)) {
|
|
113
|
+
const skillDir = path.join(layout.pluginRoot, 'skills', name);
|
|
114
|
+
const text = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
|
|
115
|
+
const { fm } = parseFrontmatter(text);
|
|
116
|
+
assert.deepEqual(Object.keys(fm).sort(), ['description', 'name'], `${name} should use Codex skill frontmatter only`);
|
|
117
|
+
assert.equal(fm.name, name);
|
|
118
|
+
assert.doesNotMatch(
|
|
119
|
+
text,
|
|
120
|
+
/~\/\.claude|CLAUDE\.md|AskUserQuestion|(^|[\s`(>])\/(?:ultra-[a-z-]+|recall|learn|gemini-collab|codex-collab)(?=$|[\s`,.;):])/m,
|
|
121
|
+
);
|
|
122
|
+
assert.doesNotMatch(text, /TaskCreate|TaskUpdate|TaskList|TaskOutput|run_in_background|~\/\.codex\/skills|mcp__claude/);
|
|
123
|
+
|
|
124
|
+
const openai = yaml.load(fs.readFileSync(path.join(skillDir, 'agents', 'openai.yaml'), 'utf8'));
|
|
125
|
+
assert.ok(openai.interface.display_name);
|
|
126
|
+
assert.ok(openai.interface.short_description);
|
|
127
|
+
assert.match(openai.interface.default_prompt, new RegExp(`\\$ultra-builder-pro:${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
|
|
128
|
+
assert.equal(typeof openai.policy.allow_implicit_invocation, 'boolean');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const cc = fs.readFileSync(path.join(layout.pluginRoot, 'skills', 'cc-collab', 'SKILL.md'), 'utf8');
|
|
132
|
+
assert.match(cc, /Claude Code/);
|
|
133
|
+
assert.match(cc, /claude --safe-mode/);
|
|
134
|
+
|
|
135
|
+
const verify = fs.readFileSync(path.join(layout.pluginRoot, 'skills', 'ultra-verify', 'SKILL.md'), 'utf8');
|
|
136
|
+
assert.match(verify, /Codex owns the task/);
|
|
137
|
+
assert.match(verify, /claude --safe-mode/);
|
|
138
|
+
assert.match(verify, /gemini --approval-mode plan/);
|
|
139
|
+
assert.match(verify, /codex-analysis\.md/);
|
|
140
|
+
assert.doesNotMatch(verify, /codex-output\.md|Claude synthesizes|Claude-only/);
|
|
141
|
+
|
|
142
|
+
const waiter = fs.readFileSync(path.join(layout.pluginRoot, 'skills', 'ultra-verify', 'scripts', 'verify_wait.py'), 'utf8');
|
|
143
|
+
assert.match(waiter, /"claude": "claude-output\.md"/);
|
|
144
|
+
assert.doesNotMatch(waiter, /"codex": "codex-output\.md"/);
|
|
145
|
+
|
|
146
|
+
const review = fs.readFileSync(path.join(layout.pluginRoot, 'skills', 'ultra-review', 'SKILL.md'), 'utf8');
|
|
147
|
+
assert.match(review, /native Codex custom agents/);
|
|
148
|
+
assert.match(review, /~\/plugins\/ultra-builder-pro\/skills\/ultra-review/);
|
|
149
|
+
|
|
150
|
+
const coreWorkflowText = [
|
|
151
|
+
...COMMANDS,
|
|
152
|
+
'ultra-review',
|
|
153
|
+
].map((name) => fs.readFileSync(path.join(layout.pluginRoot, 'skills', name, 'SKILL.md'), 'utf8')).join('\n');
|
|
154
|
+
assert.doesNotMatch(coreWorkflowText, /Phase (?:3\.7|5|7) placeholder|not yet implemented|UNKNOWN_TOOL/);
|
|
155
|
+
assert.doesNotMatch(coreWorkflowText, /session\.checkpoint|review\.run|ultra-tools subagent/);
|
|
156
|
+
assert.doesNotMatch(coreWorkflowText, /ask\.question|ultra-tools task (?:create|update|list|get)/);
|
|
157
|
+
assert.doesNotMatch(coreWorkflowText, /Context7 MCP|mcp__context7|Exa MCP|mcp__exa|Playwright via Bash/);
|
|
158
|
+
assert.doesNotMatch(coreWorkflowText, /`\/{command}`/);
|
|
159
|
+
} finally {
|
|
160
|
+
cleanup(layout);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('install converts all nine Claude agents into native Codex agent TOML', () => {
|
|
165
|
+
const layout = mkLayout();
|
|
166
|
+
try {
|
|
167
|
+
const report = install(layout);
|
|
168
|
+
assert.deepEqual(report.agents.installed.sort(), AGENTS.map((name) => `${name}.toml`).sort());
|
|
169
|
+
for (const name of AGENTS) {
|
|
170
|
+
const text = fs.readFileSync(path.join(layout.configDir, 'agents', `${name}.toml`), 'utf8');
|
|
171
|
+
assert.match(text, new RegExp(`^name = "${name}"`, 'm'));
|
|
172
|
+
assert.match(text, /^description = /m);
|
|
173
|
+
assert.match(text, /^developer_instructions = """/m);
|
|
174
|
+
assert.doesNotMatch(
|
|
175
|
+
text,
|
|
176
|
+
/model = "opus"|^tools =|^memory =|maxTurns|CLAUDE\.md|AskUserQuestion|(^|[\s`(>])\/(?:ultra-[a-z-]+|recall|learn|gemini-collab|codex-collab)(?=$|[\s`,.;):])/m,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
} finally {
|
|
180
|
+
cleanup(layout);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('plugin declares current Codex hooks and a project-local Ultra MCP server', () => {
|
|
185
|
+
const layout = mkLayout();
|
|
186
|
+
try {
|
|
187
|
+
install(layout);
|
|
188
|
+
const hooks = JSON.parse(fs.readFileSync(path.join(layout.pluginRoot, 'hooks', 'hooks.json'), 'utf8')).hooks;
|
|
189
|
+
assert.deepEqual(Object.keys(hooks).sort(), [
|
|
190
|
+
'PostCompact', 'PreCompact', 'PreToolUse', 'SessionStart', 'Stop',
|
|
191
|
+
'SubagentStart', 'SubagentStop',
|
|
192
|
+
].sort());
|
|
193
|
+
const serializedHooks = JSON.stringify(hooks);
|
|
194
|
+
for (const feature of WORKFLOW_HOOK_FILES) {
|
|
195
|
+
assert.match(serializedHooks, new RegExp(feature.replace('.', '\\.')));
|
|
196
|
+
}
|
|
197
|
+
assert.doesNotMatch(serializedHooks, /memory|recall|journal|observation_capture|user_prompt_capture|block_dangerous|post_edit_guard/);
|
|
198
|
+
assert.match(serializedHooks, /\$PLUGIN_ROOT\/hooks\/adapters\/codex\.py/);
|
|
199
|
+
|
|
200
|
+
const mcp = JSON.parse(fs.readFileSync(path.join(layout.pluginRoot, '.mcp.json'), 'utf8'));
|
|
201
|
+
const server = mcp.mcpServers['ultra-builder-pro'];
|
|
202
|
+
assert.equal(server.type, 'stdio');
|
|
203
|
+
assert.ok(path.isAbsolute(server.command));
|
|
204
|
+
assert.ok(path.isAbsolute(server.args[0]));
|
|
205
|
+
assert.equal(server.args[0], path.join(layout.pluginRoot, 'runtime', 'launch.cjs'));
|
|
206
|
+
assert.doesNotMatch(server.args[0], new RegExp(REPO_ROOT.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
207
|
+
assert.ok(!server.env, 'MCP must use each Codex task cwd so .ultra/state.db stays project-local');
|
|
208
|
+
|
|
209
|
+
for (const rel of [
|
|
210
|
+
'runtime/index.cjs',
|
|
211
|
+
'runtime/launch.cjs',
|
|
212
|
+
'runtime/ultra-tools.cjs',
|
|
213
|
+
'runtime/build/Release/better_sqlite3.node',
|
|
214
|
+
'spec/mcp-tools.yaml',
|
|
215
|
+
'spec/upstream-mcp-tools.yaml',
|
|
216
|
+
'spec/codex-capability-map.json',
|
|
217
|
+
'spec/schemas/state-db.sql',
|
|
218
|
+
'templates/.ultra/tasks/tasks.json',
|
|
219
|
+
]) {
|
|
220
|
+
assert.ok(fs.existsSync(path.join(layout.pluginRoot, rel)), `missing bundled MCP asset ${rel}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const liveSpec = yaml.load(fs.readFileSync(path.join(layout.pluginRoot, 'spec', 'mcp-tools.yaml'), 'utf8'));
|
|
224
|
+
const upstreamSpec = yaml.load(fs.readFileSync(path.join(layout.pluginRoot, 'spec', 'upstream-mcp-tools.yaml'), 'utf8'));
|
|
225
|
+
const capabilityMap = JSON.parse(fs.readFileSync(path.join(layout.pluginRoot, 'spec', 'codex-capability-map.json'), 'utf8'));
|
|
226
|
+
assert.equal(liveSpec.tools.length, 21);
|
|
227
|
+
assert.equal(upstreamSpec.tools.length, 21);
|
|
228
|
+
assert.deepEqual(upstreamSpec.tools.map((tool) => tool.name).sort(), liveSpec.tools.map((tool) => tool.name).sort());
|
|
229
|
+
assert.deepEqual(capabilityMap.live_mcp_tools.sort(), liveSpec.tools.map((tool) => tool.name).sort());
|
|
230
|
+
assert.equal(Object.keys(capabilityMap.codex_native_replacements).length, 9);
|
|
231
|
+
assert.equal(capabilityMap.codex_native_replacements['review.run'].surface, 'native_custom_agents');
|
|
232
|
+
assert.equal(capabilityMap.codex_native_replacements['ask.question'].surface, 'direct_user_interaction');
|
|
233
|
+
} finally {
|
|
234
|
+
cleanup(layout);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('bundled plugin MCP runs outside the source checkout and keeps state in the Codex task cwd', async () => {
|
|
239
|
+
const layout = mkLayout();
|
|
240
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-codex-mcp-project-'));
|
|
241
|
+
let client;
|
|
242
|
+
try {
|
|
243
|
+
install(layout);
|
|
244
|
+
const mcp = JSON.parse(fs.readFileSync(path.join(layout.pluginRoot, '.mcp.json'), 'utf8'));
|
|
245
|
+
const server = mcp.mcpServers['ultra-builder-pro'];
|
|
246
|
+
const env = { ...process.env };
|
|
247
|
+
delete env.UBP_DB_PATH;
|
|
248
|
+
delete env.UBP_ROOT_DIR;
|
|
249
|
+
delete env.UBP_RUNTIME_ROOT;
|
|
250
|
+
const transport = new StdioClientTransport({
|
|
251
|
+
command: server.command,
|
|
252
|
+
args: server.args,
|
|
253
|
+
cwd: projectDir,
|
|
254
|
+
env,
|
|
255
|
+
stderr: 'pipe',
|
|
256
|
+
});
|
|
257
|
+
client = new Client({ name: 'ubp-codex-adapter-test', version: '0.0.0' }, { capabilities: {} });
|
|
258
|
+
await client.connect(transport);
|
|
259
|
+
|
|
260
|
+
const tools = await client.listTools();
|
|
261
|
+
assert.ok(tools.tools.some((tool) => tool.name === 'task.create'));
|
|
262
|
+
const created = await client.callTool({
|
|
263
|
+
name: 'task.create',
|
|
264
|
+
arguments: { id: 'codex-bundle-1', title: 'bundled runtime', type: 'feature', priority: 'P1' },
|
|
265
|
+
});
|
|
266
|
+
assert.equal(created.isError, undefined);
|
|
267
|
+
assert.ok(fs.existsSync(path.join(projectDir, '.ultra', 'state.db')));
|
|
268
|
+
assert.ok(!fs.existsSync(path.join(layout.pluginRoot, '.ultra', 'state.db')));
|
|
269
|
+
|
|
270
|
+
const status = spawnSync(process.execPath, [
|
|
271
|
+
path.join(layout.pluginRoot, 'runtime', 'ultra-tools.cjs'),
|
|
272
|
+
'status', '--cost', '--json',
|
|
273
|
+
], { cwd: projectDir, encoding: 'utf8' });
|
|
274
|
+
assert.equal(status.status, 0, status.stderr);
|
|
275
|
+
assert.equal(JSON.parse(status.stdout).ok, true);
|
|
276
|
+
|
|
277
|
+
const freshTarget = path.join(projectDir, 'fresh-project');
|
|
278
|
+
const initialized = await client.callTool({
|
|
279
|
+
name: 'task.init_project',
|
|
280
|
+
arguments: { target_dir: freshTarget, project_name: 'Codex bundle', project_type: 'cli' },
|
|
281
|
+
});
|
|
282
|
+
assert.equal(initialized.isError, undefined);
|
|
283
|
+
assert.ok(fs.existsSync(path.join(freshTarget, '.ultra', 'tasks', 'tasks.json')));
|
|
284
|
+
} finally {
|
|
285
|
+
if (client) await client.close();
|
|
286
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
287
|
+
cleanup(layout);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('install preserves user config and marketplace entries and is byte-idempotent', () => {
|
|
292
|
+
const layout = mkLayout();
|
|
293
|
+
try {
|
|
294
|
+
fs.mkdirSync(layout.configDir, { recursive: true });
|
|
295
|
+
const configFile = path.join(layout.configDir, 'config.toml');
|
|
296
|
+
fs.writeFileSync(configFile, '[profile]\nname = "dev"\n');
|
|
297
|
+
fs.mkdirSync(path.dirname(layout.marketplaceFile), { recursive: true });
|
|
298
|
+
fs.writeFileSync(layout.marketplaceFile, JSON.stringify({
|
|
299
|
+
name: 'personal',
|
|
300
|
+
interface: { displayName: 'Personal' },
|
|
301
|
+
plugins: [{
|
|
302
|
+
name: 'mine',
|
|
303
|
+
source: { source: 'local', path: './plugins/mine' },
|
|
304
|
+
policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },
|
|
305
|
+
category: 'Developer Tools',
|
|
306
|
+
}],
|
|
307
|
+
}, null, 2) + '\n');
|
|
308
|
+
|
|
309
|
+
install(layout);
|
|
310
|
+
const firstConfig = fs.readFileSync(configFile, 'utf8');
|
|
311
|
+
const firstMarketplace = fs.readFileSync(layout.marketplaceFile, 'utf8');
|
|
312
|
+
const firstManifest = fs.readFileSync(path.join(layout.pluginRoot, '.codex-plugin', 'plugin.json'), 'utf8');
|
|
313
|
+
const firstAgent = fs.readFileSync(path.join(layout.configDir, 'agents', 'review-code.toml'), 'utf8');
|
|
314
|
+
install(layout);
|
|
315
|
+
|
|
316
|
+
assert.equal(fs.readFileSync(configFile, 'utf8'), firstConfig);
|
|
317
|
+
assert.equal(fs.readFileSync(layout.marketplaceFile, 'utf8'), firstMarketplace);
|
|
318
|
+
assert.equal(fs.readFileSync(path.join(layout.pluginRoot, '.codex-plugin', 'plugin.json'), 'utf8'), firstManifest);
|
|
319
|
+
assert.equal(fs.readFileSync(path.join(layout.configDir, 'agents', 'review-code.toml'), 'utf8'), firstAgent);
|
|
320
|
+
|
|
321
|
+
const marketplace = JSON.parse(firstMarketplace);
|
|
322
|
+
assert.deepEqual(marketplace.plugins.map((entry) => entry.name), ['mine', 'ultra-builder-pro']);
|
|
323
|
+
assert.equal(marketplace.plugins[1].source.path, './plugins/ultra-builder-pro');
|
|
324
|
+
} finally {
|
|
325
|
+
cleanup(layout);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('uninstall removes only UBP-managed plugin and agents', () => {
|
|
330
|
+
const layout = mkLayout();
|
|
331
|
+
try {
|
|
332
|
+
fs.mkdirSync(layout.configDir, { recursive: true });
|
|
333
|
+
const configFile = path.join(layout.configDir, 'config.toml');
|
|
334
|
+
fs.writeFileSync(configFile, '[profile]\nname = "dev"\n');
|
|
335
|
+
install(layout);
|
|
336
|
+
fs.writeFileSync(path.join(layout.configDir, 'agents', 'mine.toml'), 'name = "mine"\n');
|
|
337
|
+
|
|
338
|
+
codex.uninstall({
|
|
339
|
+
configDir: layout.configDir,
|
|
340
|
+
homeDir: layout.homeDir,
|
|
341
|
+
scope: 'global',
|
|
342
|
+
runPluginCli: false,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
assert.ok(!fs.existsSync(layout.pluginRoot));
|
|
346
|
+
for (const name of AGENTS) {
|
|
347
|
+
assert.ok(!fs.existsSync(path.join(layout.configDir, 'agents', `${name}.toml`)));
|
|
348
|
+
}
|
|
349
|
+
assert.ok(fs.existsSync(path.join(layout.configDir, 'agents', 'mine.toml')));
|
|
350
|
+
assert.equal(fs.readFileSync(configFile, 'utf8'), '[profile]\nname = "dev"\n');
|
|
351
|
+
const marketplace = JSON.parse(fs.readFileSync(layout.marketplaceFile, 'utf8'));
|
|
352
|
+
assert.ok(!marketplace.plugins.some((entry) => entry.name === 'ultra-builder-pro'));
|
|
353
|
+
} finally {
|
|
354
|
+
cleanup(layout);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const gemini = require('../gemini.js');
|
|
10
|
+
const { RETIRED_SKILLS, skillsForRuntime } = require('../_shared/runtime-assets.cjs');
|
|
11
|
+
|
|
12
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
13
|
+
|
|
14
|
+
function mkTarget() {
|
|
15
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-gemini-'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test('install packages into extensions/ultra-builder-pro with manifest + commands.toml + skills', () => {
|
|
19
|
+
const target = mkTarget();
|
|
20
|
+
try {
|
|
21
|
+
const r = gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
22
|
+
const extRoot = r.target;
|
|
23
|
+
assert.equal(extRoot, path.join(target, 'extensions', 'ultra-builder-pro'));
|
|
24
|
+
assert.ok(fs.existsSync(path.join(extRoot, 'gemini-extension.json')));
|
|
25
|
+
assert.ok(fs.existsSync(path.join(extRoot, 'GEMINI.md')));
|
|
26
|
+
assert.ok(r.copied.commands.includes('ultra-init.toml'));
|
|
27
|
+
assert.ok(r.copied.skills.some((p) => p.includes('ultra-init/SKILL.md')));
|
|
28
|
+
|
|
29
|
+
const installedSkills = fs.readdirSync(path.join(extRoot, 'skills')).sort();
|
|
30
|
+
assert.deepEqual(installedSkills, skillsForRuntime('gemini').sort());
|
|
31
|
+
for (const retired of RETIRED_SKILLS) assert.ok(!installedSkills.includes(retired));
|
|
32
|
+
|
|
33
|
+
const tomlContent = fs.readFileSync(path.join(extRoot, 'commands', 'ultra-init.toml'), 'utf8');
|
|
34
|
+
assert.match(tomlContent, /description = "/);
|
|
35
|
+
assert.match(tomlContent, /prompt = """/);
|
|
36
|
+
} finally {
|
|
37
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('reinstall removes stale managed skill assets while preserving runtime state', () => {
|
|
42
|
+
const target = mkTarget();
|
|
43
|
+
try {
|
|
44
|
+
gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
45
|
+
const extRoot = gemini.resolveExtensionRoot({ configDir: target });
|
|
46
|
+
const stale = path.join(extRoot, 'skills', 'recall');
|
|
47
|
+
fs.mkdirSync(stale, { recursive: true });
|
|
48
|
+
fs.writeFileSync(path.join(stale, 'SKILL.md'), 'retired');
|
|
49
|
+
fs.writeFileSync(path.join(extRoot, 'state.db'), 'preserve');
|
|
50
|
+
|
|
51
|
+
gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
52
|
+
assert.equal(fs.existsSync(stale), false);
|
|
53
|
+
assert.equal(fs.readFileSync(path.join(extRoot, 'state.db'), 'utf8'), 'preserve');
|
|
54
|
+
} finally {
|
|
55
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('manifest declares mcpServers with _ubp tag (not in env — P2 #9)', () => {
|
|
60
|
+
const target = mkTarget();
|
|
61
|
+
try {
|
|
62
|
+
gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
63
|
+
const extRoot = gemini.resolveExtensionRoot({ configDir: target });
|
|
64
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(extRoot, 'gemini-extension.json'), 'utf8'));
|
|
65
|
+
assert.equal(manifest.name, gemini.EXTENSION_NAME);
|
|
66
|
+
assert.equal(manifest._ubp.source, gemini.SOURCE_TAG);
|
|
67
|
+
assert.ok(manifest.mcpServers[gemini.MCP_SERVER_NAME]);
|
|
68
|
+
// D45: identification lives outside env so it never leaks into spawned MCP server.
|
|
69
|
+
assert.equal(manifest.mcpServers[gemini.MCP_SERVER_NAME]._ubp.source, gemini.SOURCE_TAG);
|
|
70
|
+
assert.equal(manifest.mcpServers[gemini.MCP_SERVER_NAME].env, undefined, 'project cwd must own MCP state');
|
|
71
|
+
assert.equal(manifest.contextFileName, 'GEMINI.md');
|
|
72
|
+
} finally {
|
|
73
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('manifest uses a durable bundled MCP runtime and project-local state defaults', () => {
|
|
78
|
+
const target = mkTarget();
|
|
79
|
+
try {
|
|
80
|
+
gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
81
|
+
const extRoot = gemini.resolveExtensionRoot({ configDir: target });
|
|
82
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(extRoot, 'gemini-extension.json'), 'utf8'));
|
|
83
|
+
const server = manifest.mcpServers[gemini.MCP_SERVER_NAME];
|
|
84
|
+
const launcher = server.args[0];
|
|
85
|
+
assert.ok(launcher.startsWith(path.join(extRoot, 'runtime') + path.sep));
|
|
86
|
+
assert.ok(fs.existsSync(launcher), 'bundled launcher must survive the installer source directory');
|
|
87
|
+
assert.equal(server.env, undefined, 'Gemini must resolve .ultra/state.db from the active project cwd');
|
|
88
|
+
} finally {
|
|
89
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('uninstall removes extension dir; refuses on foreign manifest', () => {
|
|
94
|
+
const target = mkTarget();
|
|
95
|
+
const extRoot = gemini.resolveExtensionRoot({ configDir: target });
|
|
96
|
+
try {
|
|
97
|
+
gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
98
|
+
assert.ok(fs.existsSync(extRoot));
|
|
99
|
+
gemini.uninstall({ configDir: target });
|
|
100
|
+
assert.ok(!fs.existsSync(extRoot));
|
|
101
|
+
|
|
102
|
+
// Re-install then tamper with the manifest's _ubp identification block.
|
|
103
|
+
gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
104
|
+
const manifestFile = path.join(extRoot, 'gemini-extension.json');
|
|
105
|
+
const m = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
106
|
+
m._ubp = { source: 'someone-else' };
|
|
107
|
+
fs.writeFileSync(manifestFile, JSON.stringify(m, null, 2));
|
|
108
|
+
assert.throws(() => gemini.uninstall({ configDir: target }), /refusing to uninstall/);
|
|
109
|
+
} finally {
|
|
110
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
});
|