Release v0.3.0

This commit is contained in:
winlifes
2026-05-20 18:52:25 -07:00
parent 3ab48e2457
commit c57cf140a8
11 changed files with 1113 additions and 33 deletions
+48
View File
@@ -0,0 +1,48 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const {
createProjectSkill,
listProjectInstructions,
readProjectInstruction,
writeProjectInstruction,
} = require('../lib/project-instructions');
test('project instruction helpers list, read, and write safe project files', () => {
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-instructions-'));
const write = writeProjectInstruction(projectPath, {
target: 'AGENTS.md',
content: '# Agent Notes\n',
});
assert.equal(write.written, true);
assert.equal(readProjectInstruction(projectPath, 'AGENTS.md').content, '# Agent Notes\n');
const listed = listProjectInstructions(projectPath);
assert.equal(listed.files.some((file) => file.path === 'AGENTS.md'), true);
});
test('createProjectSkill writes a Codex project skill', () => {
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-skill-'));
const result = createProjectSkill(projectPath, {
skillName: 'scene qa',
title: 'Scene QA',
description: 'Validate Cocos scenes.',
});
assert.equal(result.path, '.codex/skills/scene-qa/SKILL.md');
const listed = listProjectInstructions(projectPath);
assert.equal(listed.skills.some((skill) => skill.path === result.path), true);
});
test('project instruction helpers reject traversal outside the project', () => {
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-instructions-safe-'));
assert.throws(
() => writeProjectInstruction(projectPath, { target: '../AGENTS.md', content: 'x' }),
/outside the Cocos project/
);
});
+33 -7
View File
@@ -5,7 +5,7 @@ const path = require('node:path');
const test = require('node:test');
const { createToolRegistry } = require('../lib/tool-registry');
function createRegistry(profile, projectPath = path.resolve('/tmp/funplay-cocos-test-project'), configExtras = {}) {
function createRegistry(profile, projectPath = path.resolve('/tmp/funplay-cocos-test-project'), configExtras = {}, overrides = {}) {
return createToolRegistry({
getRuntimeContext: () => ({
config: { toolProfile: profile, ...configExtras },
@@ -13,31 +13,42 @@ function createRegistry(profile, projectPath = path.resolve('/tmp/funplay-cocos-
version: '0.0.0-test',
}),
interactionLog: { add() {} },
runtimeLog: { list: () => [], clear: () => 0 },
sceneBridge: { call: async () => ({ ok: true }) },
editorExecutor: async () => ({ ok: true }),
runtimeLog: { add() {}, list: () => [], clear: () => 0 },
sceneBridge: overrides.sceneBridge || { call: async () => ({ ok: true }) },
editorExecutor: overrides.editorExecutor || (async () => ({ ok: true })),
});
}
test('core profile exposes the documented focused tool set', () => {
const tools = createRegistry('core').listTools();
assert.equal(tools.length, 28);
assert.equal(tools.length, 34);
assert.equal(tools.some((tool) => tool.name === 'execute_javascript'), true);
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
assert.equal(tools.some((tool) => tool.name === 'get_tool_catalog'), true);
assert.equal(tools.some((tool) => tool.name === 'validate_scene'), true);
assert.equal(tools.some((tool) => tool.name === 'get_performance_snapshot'), true);
assert.equal(tools.some((tool) => tool.name === 'list_project_instructions'), true);
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
assert.equal(tools.some((tool) => tool.name === 'write_file'), false);
});
test('full profile exposes all built-in tools', () => {
const tools = createRegistry('full').listTools();
assert.equal(tools.length, 76);
assert.equal(tools.length, 89);
assert.equal(tools.some((tool) => tool.name === 'write_file'), true);
assert.equal(tools.some((tool) => tool.name === 'edit_prefab_json'), true);
assert.equal(tools.some((tool) => tool.name === 'create_project_skill'), true);
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
});
test('tool definitions include MCP outputSchema and annotations', () => {
const tool = createRegistry('core').listTools().find((item) => item.name === 'get_project_info');
assert.equal(tool.outputSchema.type, 'object');
assert.equal(tool.outputSchema.properties.ok.type, 'boolean');
assert.equal(tool.annotations.readOnlyHint, true);
});
test('custom profile can expose a category and disable a specific tool', () => {
const tools = createRegistry('custom', path.resolve('/tmp/funplay-cocos-test-project'), {
enabledToolCategories: ['files'],
@@ -69,6 +80,21 @@ test('file tools reject writes outside the project root', async () => {
test('callToolDetailed preserves structured values and text output', async () => {
const registry = createRegistry('core');
const result = await registry.callToolDetailed('get_project_info', {});
assert.equal(result.value.projectPath, path.resolve('/tmp/funplay-cocos-test-project'));
assert.equal(result.value.ok, true);
assert.equal(result.value.tool, 'get_project_info');
assert.equal(result.value.data.projectPath, path.resolve('/tmp/funplay-cocos-test-project'));
assert.match(result.value.callId, /^fp_/);
assert.match(result.text, /projectPath/);
});
test('callToolDetailed preserves screenshot image text while keeping structured envelope small', async () => {
const dataUri = 'data:image/png;base64,AAAA';
const registry = createRegistry('core', path.resolve('/tmp/funplay-cocos-test-project'), {}, {
editorExecutor: async () => dataUri,
});
const result = await registry.callToolDetailed('execute_javascript', { context: 'editor', code: 'return image;' });
assert.equal(result.text, dataUri);
assert.equal(result.value.data.image, true);
assert.equal(result.value.data.mimeType, 'image/png');
});