@givolt/i3-composer-agent 2.3.1 → 2.3.2
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/.agents/plugins/marketplace.json +1 -1
- package/.claude/commands/i3-prototype.md +5 -7
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini/commands/i3-prototype.toml +5 -7
- package/README.md +12 -7
- package/bin/i3-composer-agent.cjs +33 -2
- package/bin/install.cjs +14 -0
- package/bin/native-migration.cjs +27 -3
- package/commands/i3-prototype.toml +5 -7
- package/docs/codebuddy-setup.md +65 -2
- package/docs/codex-setup.md +4 -0
- package/docs/other-hosts.md +1 -1
- package/docs/ui-prototype-native/MAIN-SESSION-REPORT.md +66 -0
- package/docs/ui-prototype-native/USAGE.md +91 -0
- package/docs/workbuddy-setup.md +4 -0
- package/evals/cases/i3-frontend-ui-engineering.json +16 -0
- package/evals/cases/i3-ui-prototype.json +109 -0
- package/evals/cases/i3-using-agent-skills.json +28 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css +3 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js +8 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json +67 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/index.html +89 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/readme.md +52 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/template.json +8 -0
- package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +17 -0
- package/evals/zh-CN/cases/i3-ui-prototype.json +115 -0
- package/evals/zh-CN/cases/i3-using-agent-skills.json +30 -0
- package/package.json +31 -1
- package/plugin.json +1 -1
- package/scripts/design-provider-test.js +7 -0
- package/scripts/enterprise-template-test.js +27 -1
- package/scripts/lib/prototype-fixture.cjs +17 -0
- package/scripts/prototype-locations-test.js +58 -0
- package/scripts/ui-prototype-content-test.js +123 -0
- package/scripts/ui-prototype-dependencies-test.js +213 -0
- package/scripts/ui-prototype-devtools-test.js +53 -0
- package/scripts/ui-prototype-native-test.js +504 -0
- package/scripts/ui-prototype-template-test.js +261 -0
- package/scripts/ui-prototype-test.js +10 -0
- package/skills/i3-ui-prototype/SKILL.md +138 -19
- package/skills/i3-ui-prototype/assets/capabilities/management.json +294 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/acceptance.json +51 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/component-map.json +13 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/index.html +11 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package-lock.json +2298 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package.json +22 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/public/prototype-config.json +6 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/App.vue +136 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/components/DeviceEditor.vue +111 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/config.ts +19 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/content.ts +28 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/devices.ts +50 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/theme.ts +25 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/main.ts +18 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/style.css +50 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/useDevices.ts +66 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/devices.test.ts +44 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/theme.test.ts +26 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tsconfig.json +15 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/vite.config.ts +4 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/profile.json +174 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/readme.md +61 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/index.html +11 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package-lock.json +2298 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package.json +22 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json +21 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +51 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +18 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/theme.ts +25 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/main.ts +18 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +13 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/theme.test.ts +26 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tsconfig.json +15 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/vite.config.ts +4 -0
- package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +737 -0
- package/skills/i3-ui-prototype/assets/optional-skills/sources.json +27 -0
- package/skills/i3-ui-prototype/assets/templates/catalog.js +149 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline-build.json +265 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline.js +8888 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template.json +9 -0
- package/skills/i3-ui-prototype/assets/templates/gallery/index.html +4 -4
- package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
- package/skills/i3-ui-prototype/references/artifact-contract.md +25 -0
- package/skills/i3-ui-prototype/references/content-template.md +76 -0
- package/skills/i3-ui-prototype/references/enterprise-admin.md +6 -0
- package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
- package/skills/i3-ui-prototype/references/expert-adapter.md +33 -5
- package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
- package/skills/i3-ui-prototype/references/interaction-acceptance.md +165 -0
- package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
- package/skills/i3-ui-prototype/references/optional-skills.md +59 -0
- package/skills/i3-ui-prototype/references/template-adapters.md +1 -1
- package/skills/i3-ui-prototype/references/workflow-contract.md +58 -4
- package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
- package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +13 -6
- package/skills/i3-ui-prototype/scripts/manage-baseline.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/native-browser.cjs +785 -0
- package/skills/i3-ui-prototype/scripts/native-build.cjs +91 -0
- package/skills/i3-ui-prototype/scripts/native-delivery.cjs +91 -0
- package/skills/i3-ui-prototype/scripts/native-devtools.cjs +91 -0
- package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
- package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +244 -0
- package/skills/i3-ui-prototype/scripts/native-pilot.cjs +34 -0
- package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
- package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
- package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +26 -6
- package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +65 -0
- package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
- package/skills/i3-ui-prototype/scripts/template-contract.cjs +398 -0
- package/skills/i3-ui-prototype/scripts/validate-result.cjs +117 -0
- package/skills/i3-using-agent-skills/SKILL.md +2 -5
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const installation = path.join(__dirname, 'lib/installation.js');
|
|
8
|
+
const skills = fs.existsSync(installation) ? require(installation).loadInstallation().skillsDirectory : path.resolve(__dirname, '../skills');
|
|
9
|
+
const deps = require(path.join(skills, 'i3-ui-prototype/scripts/prototype-dependencies.cjs'));
|
|
10
|
+
const installer = require(path.join(skills, 'i3-ui-prototype/scripts/optional-skill-install.cjs'));
|
|
11
|
+
function workspace(t) {
|
|
12
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'i3-optional-deps-')));
|
|
13
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
14
|
+
const project = path.join(root, 'business'), home = path.join(root, 'home');
|
|
15
|
+
fs.mkdirSync(project); fs.mkdirSync(home);
|
|
16
|
+
return { project, home, env: {
|
|
17
|
+
...process.env,
|
|
18
|
+
HOME: home, USERPROFILE: home,
|
|
19
|
+
XDG_CONFIG_HOME: path.join(home, '.config'),
|
|
20
|
+
CODEBUDDY_CONFIG_DIR: path.join(home, '.codebuddy'),
|
|
21
|
+
WORKBUDDY_CONFIG_DIR: path.join(home, '.workbuddy'),
|
|
22
|
+
CLAUDE_CONFIG_DIR: path.join(home, '.claude'),
|
|
23
|
+
CODEX_HOME: path.join(home, '.codex'),
|
|
24
|
+
} };
|
|
25
|
+
}
|
|
26
|
+
test('missing optional Skills do not fail read-only inspection or write files', t => {
|
|
27
|
+
const options = workspace(t), before = fs.readdirSync(options.project);
|
|
28
|
+
const result = deps.check(options);
|
|
29
|
+
assert.equal(result.status, 'checked');
|
|
30
|
+
assert.equal(result.professionalReady, false);
|
|
31
|
+
assert.equal(result.antdv.skill.availability, 'missing');
|
|
32
|
+
assert.equal(result.antdv.tool.availability, 'missing');
|
|
33
|
+
assert.deepEqual(fs.readdirSync(options.project), before);
|
|
34
|
+
});
|
|
35
|
+
test('later installed antDv Skill is detected without reinstalling i3 or claiming the CLI ready', t => {
|
|
36
|
+
const options = workspace(t);
|
|
37
|
+
assert.equal(deps.check(options).antdv.skill.availability, 'missing');
|
|
38
|
+
const root = path.join(options.project, '.codebuddy/skills/antdv');
|
|
39
|
+
fs.mkdirSync(root, { recursive: true }); fs.writeFileSync(path.join(root, 'SKILL.md'), 'official Skill fixture');
|
|
40
|
+
const result = deps.check(options);
|
|
41
|
+
assert.equal(result.antdv.skill.root, root);
|
|
42
|
+
assert.equal(result.antdv.skill.availability, 'available');
|
|
43
|
+
assert.equal(result.antdv.skill.invocationVerified, false);
|
|
44
|
+
assert.equal(result.antdv.tool.availability, 'missing');
|
|
45
|
+
});
|
|
46
|
+
test('CodeBuddy global antDv is reusable and current project entry has precedence', t => {
|
|
47
|
+
const options = workspace(t);
|
|
48
|
+
options.env.CODEBUDDY_CONFIG_DIR = path.join(options.home, 'legacy CodeBuddy config');
|
|
49
|
+
const global = path.join(options.env.CODEBUDDY_CONFIG_DIR, 'skills/antdv');
|
|
50
|
+
fs.mkdirSync(global, { recursive: true }); fs.writeFileSync(path.join(global, 'SKILL.md'), 'global');
|
|
51
|
+
assert.equal(deps.skillAt(options.project, 'antdv', options).source, 'global-skill');
|
|
52
|
+
const local = path.join(options.project, '.codebuddy/skills/antdv');
|
|
53
|
+
fs.mkdirSync(local, { recursive: true }); fs.writeFileSync(path.join(local, 'SKILL.md'), 'local');
|
|
54
|
+
assert.equal(deps.skillAt(options.project, 'antdv', options).root, local);
|
|
55
|
+
});
|
|
56
|
+
test('global UIUX detection reuses enabled user plugins, preserves disabled status and excludes business plugins', t => {
|
|
57
|
+
const options = { ...workspace(t), host: 'codebuddy', scope: 'global' }, config = path.join(options.home, '.codebuddy');
|
|
58
|
+
const plugin = path.join(options.home, 'uiux-plugin'), root = path.join(plugin, 'skills/ui-ux-pro-max'), id = 'ui-ux-pro-max@fixture';
|
|
59
|
+
const write = (file, content) => { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, typeof content === 'string' ? content : JSON.stringify(content)); };
|
|
60
|
+
write(path.join(plugin, '.codebuddy-plugin/plugin.json'), { name: 'ui-ux-pro-max', version: '2.15.0' });
|
|
61
|
+
for (const file of ['SKILL.md', 'scripts/core.py', 'scripts/design_system.py', ...['ux-guidelines','styles','colors','typography','products','landing','ui-reasoning'].map(name => `data/${name}.csv`)]) write(path.join(root, file), 'synthetic fixture');
|
|
62
|
+
write(path.join(root, 'scripts/search.py'), 'import json\nprint(json.dumps({"domain":"ux","results":[{"id":"synthetic-probe-only"}]}))\n');
|
|
63
|
+
write(path.join(config, 'settings.json'), { enabledPlugins: { [id]: true } });
|
|
64
|
+
const record = { scope: 'user', installPath: plugin, version: '2.15.0' };
|
|
65
|
+
write(path.join(config, 'plugins/installed_plugins.json'), { version: 2, plugins: { [id]: [record] } });
|
|
66
|
+
const result = deps.check(options);
|
|
67
|
+
assert.equal(result.uiux.root, root);
|
|
68
|
+
assert.equal(result.uiux.availability, 'available');
|
|
69
|
+
assert.equal(result.uiux.source, 'codebuddy-plugin');
|
|
70
|
+
write(path.join(config, 'settings.json'), { enabledPlugins: { [id]: false } });
|
|
71
|
+
assert.equal(deps.check(options).uiux.availability, 'disabled');
|
|
72
|
+
write(path.join(config, 'settings.json'), { enabledPlugins: { [id]: true } });
|
|
73
|
+
write(path.join(config, 'plugins/installed_plugins.json'), { version: 2, plugins: { [id]: [{ ...record, scope: 'project', projectPath: options.project }] } });
|
|
74
|
+
assert.equal(deps.check(options).uiux.availability, 'missing');
|
|
75
|
+
});
|
|
76
|
+
test('dependency CLI rejects unintended writes and unrelated hosts', () => {
|
|
77
|
+
assert.throws(() => deps.parse(['check', '--force']), /Invalid option/);
|
|
78
|
+
assert.throws(() => deps.check({ host: 'not-a-host' }), /Unknown host/);
|
|
79
|
+
assert.throws(() => deps.parse(['update']), /check or install/);
|
|
80
|
+
});
|
|
81
|
+
function officialFixture(name, folder) {
|
|
82
|
+
const skill = path.join(folder, 'skill'), tool = path.join(folder, 'tool');
|
|
83
|
+
fs.mkdirSync(skill); fs.mkdirSync(tool);
|
|
84
|
+
fs.writeFileSync(path.join(skill, 'SKILL.md'), `---\nname: ${name === 'uiux' ? 'ui-ux-pro-max' : 'antdv'}\n---\nOfficial fixture; not host invocation evidence.\n`);
|
|
85
|
+
if (name === 'uiux') {
|
|
86
|
+
for (const file of ['scripts/search.py', 'scripts/core.py', 'scripts/design_system.py', 'data/ux-guidelines.csv']) {
|
|
87
|
+
fs.mkdirSync(path.dirname(path.join(skill, file)), { recursive: true }); fs.writeFileSync(path.join(skill, file), 'fixture');
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
const pkg = path.join(tool, 'node_modules/@antdv-next/cli'); fs.mkdirSync(pkg, { recursive: true });
|
|
91
|
+
fs.writeFileSync(path.join(pkg, 'package.json'), JSON.stringify({ name: '@antdv-next/cli', version: '0.0.0-beta.1', bin: { antdv: 'cli.cjs' } }));
|
|
92
|
+
fs.writeFileSync(path.join(pkg, 'cli.cjs'), 'process.stdout.write(JSON.stringify({name:"Table",props:[]}));');
|
|
93
|
+
}
|
|
94
|
+
return { skill, tool };
|
|
95
|
+
}
|
|
96
|
+
test('explicit skip never downloads, writes or permanently disables dependencies', async t => {
|
|
97
|
+
const options = workspace(t), before = fs.readdirSync(options.project);
|
|
98
|
+
const result = await installer.install({ ...options, skills: 'none' }, { prepare() { throw new Error('Unexpected download'); } });
|
|
99
|
+
assert.equal(result.status, 'skipped');
|
|
100
|
+
assert.deepEqual(fs.readdirSync(options.project), before);
|
|
101
|
+
assert.deepEqual(installer.selection('uiux,antdv'), ['uiux', 'antdv']);
|
|
102
|
+
assert.throws(() => installer.selection('none,uiux'), /Choose/);
|
|
103
|
+
});
|
|
104
|
+
test('optional antDv installation preserves user files and a later invocation reuses the Skill', async t => {
|
|
105
|
+
const options = workspace(t);
|
|
106
|
+
fs.writeFileSync(path.join(options.project, 'PRD.md'), 'human requirements');
|
|
107
|
+
const services = { prepare: officialFixture, progress() {} };
|
|
108
|
+
const result = await installer.install({ ...options, skills: 'antdv' }, services);
|
|
109
|
+
assert.equal(result.status, 'installed');
|
|
110
|
+
assert.equal(result.detection.antdv.skill.availability, 'available');
|
|
111
|
+
assert.equal(fs.readFileSync(path.join(options.project, 'PRD.md'), 'utf8'), 'human requirements');
|
|
112
|
+
const skillFile = path.join(options.project, '.codebuddy/skills/antdv/SKILL.md');
|
|
113
|
+
fs.appendFileSync(skillFile, 'user edit'); const before = fs.readFileSync(skillFile);
|
|
114
|
+
const again = await installer.install({ ...options, skills: 'antdv' }, { prepare() { throw new Error('Existing tool must not be replaced'); } });
|
|
115
|
+
assert.equal(again.status, 'reused');
|
|
116
|
+
assert.deepEqual(fs.readFileSync(skillFile), before);
|
|
117
|
+
});
|
|
118
|
+
test('a failed second preparation publishes neither selected Skill and removes temporary files', async t => {
|
|
119
|
+
const options = workspace(t);
|
|
120
|
+
await assert.rejects(installer.install({ ...options, skills: 'uiux,antdv' }, {
|
|
121
|
+
progress() {}, prepare(name, folder) { if (name === 'antdv') throw new Error('simulated network failure'); return officialFixture(name, folder); },
|
|
122
|
+
}), /simulated network failure/);
|
|
123
|
+
assert.deepEqual(fs.readdirSync(options.project), []);
|
|
124
|
+
});
|
|
125
|
+
test('antDv error text or error JSON with exit zero is not successful knowledge', async t => {
|
|
126
|
+
const options = workspace(t);
|
|
127
|
+
await installer.install({ ...options, skills: 'antdv' }, { prepare: officialFixture, progress() {} });
|
|
128
|
+
const file = path.join(resultTool(options), 'node_modules/@antdv-next/cli/cli.cjs');
|
|
129
|
+
for (const output of ['Error: Table knowledge missing', JSON.stringify({ error: 'Table knowledge missing' })]) {
|
|
130
|
+
fs.writeFileSync(file, `process.stdout.write(${JSON.stringify(output)});`);
|
|
131
|
+
assert.equal(deps.check(options).antdv.tool.availability, 'unusable');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
function resultTool(options) { return path.join(deps.check(options).locations.toolsDirectory, 'antdv-cli'); }
|
|
135
|
+
for (const host of ['codebuddy', 'workbuddy', 'codex', 'claude-code']) for (const scope of ['project', 'global']) {
|
|
136
|
+
test(`${host} ${scope} optional installation keeps all tools inside the selected host root`, async t => {
|
|
137
|
+
const options = { ...workspace(t), host, scope };
|
|
138
|
+
const result = await installer.install({ ...options, skills: 'antdv' }, { prepare: officialFixture, progress() {} });
|
|
139
|
+
assert.equal(result.detection.antdv.tool.source, `${scope}-tool`);
|
|
140
|
+
assert.ok(fs.existsSync(path.join(resultTool(options), 'node_modules/@antdv-next/cli/package.json')));
|
|
141
|
+
assert.equal(fs.existsSync(path.join(options.project, '.prototype-tools')), false);
|
|
142
|
+
assert.equal(fs.existsSync(path.join(options.project, 'package.json')), false);
|
|
143
|
+
if (scope === 'global') assert.deepEqual(fs.readdirSync(options.project), []);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
test('an available global tool is reused by a later project without project writes', async t => {
|
|
147
|
+
const options = workspace(t);
|
|
148
|
+
await installer.install({ ...options, host: 'codex', scope: 'global', skills: 'antdv' }, { prepare: officialFixture, progress() {} });
|
|
149
|
+
const result = await installer.install({ ...options, host: 'codex', skills: 'antdv' }, { prepare() { throw new Error('No download expected'); } });
|
|
150
|
+
assert.equal(result.status, 'reused');
|
|
151
|
+
assert.equal(result.detection.antdv.tool.source, 'global-tool');
|
|
152
|
+
assert.deepEqual(fs.readdirSync(options.project), []);
|
|
153
|
+
});
|
|
154
|
+
test('legacy independent tools stay readable until an explicit owned migration', async t => {
|
|
155
|
+
const options = workspace(t), legacy = path.join(options.project, '.prototype-tools/antdv-cli');
|
|
156
|
+
const stage = path.join(options.project, 'fixture'); fs.mkdirSync(stage);
|
|
157
|
+
const prepared = officialFixture('antdv', stage);
|
|
158
|
+
fs.mkdirSync(path.dirname(legacy)); fs.renameSync(prepared.tool, legacy); fs.rmSync(stage, { recursive: true });
|
|
159
|
+
fs.writeFileSync(path.join(legacy, 'UPSTREAM-ORIGIN.json'), JSON.stringify({ ...require(path.join(skills, 'i3-ui-prototype/assets/optional-skills/sources.json')).antdv, installer: 'i3 optional dependency preparation; official Skill body unchanged' }));
|
|
160
|
+
assert.equal(deps.check(options).antdv.tool.source, 'legacy-project-tool');
|
|
161
|
+
await installer.install({ ...options, skills: 'antdv' }, { prepare: officialFixture, progress() {} });
|
|
162
|
+
assert.equal(fs.existsSync(legacy), true);
|
|
163
|
+
fs.writeFileSync(path.join(options.project, '.prototype-tools/user-note.txt'), 'keep this location');
|
|
164
|
+
await installer.install({ ...options, scope: 'global', skills: 'antdv' }, { prepare: officialFixture, progress() {} });
|
|
165
|
+
assert.equal(deps.check(options).antdv.tool.source, 'global-tool', 'normal lookup prefers a current global tool over legacy');
|
|
166
|
+
const result = await installer.install({ ...options, skills: 'antdv', migrateTools: true }, { prepare() { throw new Error('No download expected'); } });
|
|
167
|
+
assert.equal(result.status, 'migrated');
|
|
168
|
+
assert.equal(result.detection.antdv.tool.source, 'project-tool');
|
|
169
|
+
assert.equal(fs.existsSync(legacy), false);
|
|
170
|
+
assert.equal(fs.readFileSync(path.join(options.project, '.prototype-tools/user-note.txt'), 'utf8'), 'keep this location');
|
|
171
|
+
});
|
|
172
|
+
test('legacy mixed tools without i3 ownership remain untouched by migration', async t => {
|
|
173
|
+
const options = workspace(t), stage = path.join(options.project, 'fixture'); fs.mkdirSync(stage);
|
|
174
|
+
const prepared = officialFixture('antdv', stage), legacy = path.join(options.project, '.prototype-tools/antdv-cli');
|
|
175
|
+
fs.mkdirSync(path.dirname(legacy)); fs.renameSync(prepared.tool, legacy); fs.rmSync(stage, { recursive: true });
|
|
176
|
+
await assert.rejects(installer.install({ ...options, skills: 'antdv', migrateTools: true }, { prepare() { throw new Error('No download expected'); } }), /ownership|来源|归属/);
|
|
177
|
+
assert.equal(deps.check(options).antdv.tool.source, 'legacy-project-tool');
|
|
178
|
+
});
|
|
179
|
+
test('a broken project tool is reported instead of silently borrowing a healthy global tool', async t => {
|
|
180
|
+
const options = workspace(t);
|
|
181
|
+
await installer.install({ ...options, scope: 'global', skills: 'antdv' }, { prepare: officialFixture, progress() {} });
|
|
182
|
+
const local = path.join(options.project, '.codebuddy/tools/i3-prototype/antdv-cli/node_modules/@antdv-next/cli');
|
|
183
|
+
fs.mkdirSync(local, { recursive: true }); fs.writeFileSync(path.join(local, 'package.json'), '{broken');
|
|
184
|
+
assert.equal(deps.check(options).antdv.tool.availability, 'unusable');
|
|
185
|
+
assert.equal(deps.check({ ...options, scope: 'global' }).antdv.tool.availability, 'available');
|
|
186
|
+
await assert.rejects(installer.install({ ...options, skills: 'antdv' }), /Existing tool unusable/);
|
|
187
|
+
assert.equal(fs.readFileSync(path.join(local, 'package.json'), 'utf8'), '{broken');
|
|
188
|
+
});
|
|
189
|
+
test('failed post-migration execution restores the old location and removes newly created destinations', async t => {
|
|
190
|
+
const options = workspace(t), stage = path.join(options.project, 'fixture'); fs.mkdirSync(stage);
|
|
191
|
+
const prepared = officialFixture('antdv', stage), legacy = path.join(options.project, '.prototype-tools/antdv-cli');
|
|
192
|
+
fs.mkdirSync(path.dirname(legacy)); fs.renameSync(prepared.tool, legacy); fs.rmSync(stage, { recursive: true });
|
|
193
|
+
const file = path.join(legacy, 'node_modules/@antdv-next/cli/cli.cjs');
|
|
194
|
+
fs.writeFileSync(file, 'if(__dirname.includes("tools/i3-prototype"))process.exit(1);process.stdout.write(JSON.stringify({name:"Table",props:[]}));');
|
|
195
|
+
fs.writeFileSync(path.join(legacy, 'UPSTREAM-ORIGIN.json'), JSON.stringify({ ...require(path.join(skills, 'i3-ui-prototype/assets/optional-skills/sources.json')).antdv, installer: 'i3 optional dependency preparation; official Skill body unchanged' }));
|
|
196
|
+
await assert.rejects(installer.install({ ...options, skills: 'antdv', migrateTools: true }, { prepare: officialFixture, progress() {} }), /tool query failed/);
|
|
197
|
+
assert.equal(deps.check(options).antdv.tool.source, 'legacy-project-tool');
|
|
198
|
+
assert.equal(fs.existsSync(path.join(options.project, '.codebuddy')), false);
|
|
199
|
+
assert.ok(fs.existsSync(file));
|
|
200
|
+
});
|
|
201
|
+
test('unusable existing UIUX resources are preserved without attempting a download', async t => {
|
|
202
|
+
const options = workspace(t), target = path.join(options.project, '.codebuddy/skills/ui-ux-pro-max');
|
|
203
|
+
fs.mkdirSync(target, { recursive: true }); fs.writeFileSync(path.join(target, 'SKILL.md'), 'human-owned incomplete skill');
|
|
204
|
+
await assert.rejects(installer.install({ ...options, skills: 'uiux' }, { prepare() { throw new Error('must not download'); } }), /Existing Skill preserved/);
|
|
205
|
+
assert.equal(fs.readFileSync(path.join(target, 'SKILL.md'), 'utf8'), 'human-owned incomplete skill');
|
|
206
|
+
});
|
|
207
|
+
test('optional installer rejects symlink escapes, including dangling targets, and bad download bytes', async t => {
|
|
208
|
+
const options = workspace(t), outside = path.join(options.project, 'outside');
|
|
209
|
+
fs.symlinkSync(outside, path.join(options.project, '.codebuddy'), 'dir');
|
|
210
|
+
await assert.rejects(installer.install({ ...options, skills: 'antdv' }, { prepare: officialFixture }), /symlink/);
|
|
211
|
+
assert.equal(fs.existsSync(outside), false);
|
|
212
|
+
assert.throws(() => installer.verify(Buffer.from('altered'), '0'.repeat(64)), /integrity mismatch/);
|
|
213
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),os=require('node:os');
|
|
3
|
+
const installationFile=path.join(__dirname,'lib/installation.js');
|
|
4
|
+
const skills=fs.existsSync(installationFile)?require(installationFile).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills');
|
|
5
|
+
const review=require(path.join(skills,'i3-ui-prototype/scripts/native-devtools.cjs'));
|
|
6
|
+
// Synthetic contract fixtures only; none is browser or host invocation evidence.
|
|
7
|
+
function fixture(t){
|
|
8
|
+
const directory=fs.mkdtempSync(path.join(os.tmpdir(),'devtools-review-'));t.after(()=>fs.rmSync(directory,{recursive:true,force:true}));
|
|
9
|
+
const write=(file,value)=>{const full=path.join(directory,file);fs.mkdirSync(path.dirname(full),{recursive:true});fs.writeFileSync(full,Buffer.isBuffer(value)||typeof value==='string'?value:JSON.stringify(value));};
|
|
10
|
+
const current={revision:'r001',digest:'a'.repeat(64)},build={buildId:'b-fixture',buildDigest:'b'.repeat(64)};
|
|
11
|
+
write('inputs/acceptance.json',{browserReview:review.policy()});
|
|
12
|
+
const png=Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=','base64');
|
|
13
|
+
const observations={};for(const kind of review.KINDS){const evidence=`qa/fixture/${kind}.${kind==='screenshot'?'png':kind==='performance'?'json':'txt'}`;write(evidence,kind==='screenshot'?png:kind==='performance'?{traceEvents:[{name:'synthetic-fixture',ts:1}]}:`Synthetic ${kind} fixture; not executed`);observations[kind]={status:'passed',summary:'Synthetic guard fixture, not executed',evidence};}
|
|
14
|
+
const report={schemaVersion:1,status:'passed',revision:current.revision,candidateDigest:current.digest,...build,executor:'synthetic-unit-fixture',startedAt:'2026-10-17T00:00:00.000Z',completedAt:'2026-10-17T00:00:01.000Z',previewBaseURL:'http://127.0.0.1:4173/',skill:{name:review.policy().skill,toolCallId:'fixture-skill',evidence:'synthetic-fixture only'},mcp:{name:'chrome-devtools-mcp',version:'1.11.0',evidence:'synthetic-fixture only',calls:['evaluate_script','take_screenshot','list_console_messages','list_network_requests','take_snapshot','performance_start_trace','performance_stop_trace'].map((name,index)=>({tool:`mcp__chrome-devtools__${name}`,toolCallId:`fixture-${index}`}))},observations,openItems:[]};
|
|
15
|
+
const lib={manifest:()=>current,verifyBuild:(_root,id)=>{assert.equal(id,build.buildId);return build;},claim:()=>{},release:()=>{}};
|
|
16
|
+
return {directory,write,current,build,report,lib};
|
|
17
|
+
}
|
|
18
|
+
test('new DevTools policy is explicit; old fixed inputs remain read-only and not required',t=>{
|
|
19
|
+
const f=fixture(t);assert.equal(review.check(f.directory,f.lib).status,'not-run');
|
|
20
|
+
f.write('inputs/acceptance.json',{});const before=fs.readdirSync(f.directory);assert.deepEqual(review.check(f.directory,f.lib),{status:'not-required',required:false});assert.deepEqual(fs.readdirSync(f.directory),before);
|
|
21
|
+
});
|
|
22
|
+
test('fixed framework inputs retain all six DevTools observations and reject policy removal',t=>{
|
|
23
|
+
const f=fixture(t);f.write('inputs/framework.json',{acceptance:{browserReview:review.policy()}});
|
|
24
|
+
for(const mutate of [value=>delete value.browserReview,value=>value.browserReview.required=false,value=>value.browserReview.observations.pop()]){
|
|
25
|
+
const input={browserReview:review.policy()};mutate(input);f.write('inputs/acceptance.json',input);
|
|
26
|
+
assert.equal(review.check(f.directory,f.lib).status,'failed');assert.throws(()=>review.record(f.directory,f.report,f.lib),/policy|observations|opt out/);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
test('a review binds six actual-format evidence files to current candidate and build without attesting invocation',t=>{
|
|
30
|
+
const f=fixture(t),before=f.current.digest;const result=review.record(f.directory,f.report,f.lib);
|
|
31
|
+
assert.equal(result.status,'passed');assert.equal(result.invocationVerified,false);assert.equal(f.current.digest,before);
|
|
32
|
+
const checked=review.check(f.directory,f.lib);assert.equal(checked.status,'current');assert.equal(Object.keys(checked.evidenceDigests).length,6);
|
|
33
|
+
assert.match(checked.limitations,/host transcript/);
|
|
34
|
+
});
|
|
35
|
+
test('missing observation, stale identity and substituted evidence cannot pass the review gate',t=>{
|
|
36
|
+
const f=fixture(t);
|
|
37
|
+
for(const mutate of [r=>delete r.observations.network,r=>r.observations.console.status='not-run',r=>r.candidateDigest='c'.repeat(64),r=>r.buildDigest='c'.repeat(64),r=>r.skill.name='other-skill',r=>r.mcp.calls=r.mcp.calls.filter(c=>!c.tool.endsWith('performance_stop_trace')),r=>r.observations.network.evidence=r.observations.dom.evidence]){
|
|
38
|
+
const report=structuredClone(f.report);mutate(report);assert.throws(()=>review.record(f.directory,report,f.lib));assert.equal(fs.existsSync(path.join(f.directory,'qa/devtools-review.json')),false);
|
|
39
|
+
}
|
|
40
|
+
review.record(f.directory,f.report,f.lib);f.write(f.report.observations.network.evidence,'changed QA artifact');assert.equal(review.check(f.directory,f.lib).status,'failed');
|
|
41
|
+
});
|
|
42
|
+
test('Playwright ZIP and mislabeled screenshots do not become DevTools trace or PNG evidence',t=>{
|
|
43
|
+
const f=fixture(t);f.write(f.report.observations.performance.evidence,Buffer.from('PK\x03\x04fake-playwright-trace'));assert.throws(()=>review.record(f.directory,f.report,f.lib),/trace|JSON/);
|
|
44
|
+
f.write(f.report.observations.performance.evidence,{traceEvents:[{name:'fixture'}]});f.write(f.report.observations.screenshot.evidence,'not a PNG');assert.throws(()=>review.record(f.directory,f.report,f.lib),/PNG/);
|
|
45
|
+
});
|
|
46
|
+
test('a truncated PNG header is not a complete screenshot artifact',t=>{
|
|
47
|
+
const f=fixture(t),fake=Buffer.alloc(24);Buffer.from([137,80,78,71,13,10,26,10]).copy(fake);fake.write('IHDR',12);fake.writeUInt32BE(1,16);fake.writeUInt32BE(1,20);f.write(f.report.observations.screenshot.evidence,fake);
|
|
48
|
+
assert.throws(()=>review.record(f.directory,f.report,f.lib),/PNG/);
|
|
49
|
+
});
|
|
50
|
+
test('evidence paths cannot escape QA through traversal or symlinks',t=>{
|
|
51
|
+
const f=fixture(t),r=structuredClone(f.report);r.observations.dom.evidence='../outside.txt';assert.throws(()=>review.record(f.directory,r,f.lib),/QA|path/);
|
|
52
|
+
const link=path.join(f.directory,'qa/link.txt');fs.symlinkSync(path.join(f.directory,f.report.observations.dom.evidence),link);r.observations.dom.evidence='qa/link.txt';assert.throws(()=>review.record(f.directory,r,f.lib),/symlink/);
|
|
53
|
+
});
|