@autobest-ui/agent 1.0.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/README.md +182 -0
- package/bin/sync-assets.mjs +126 -0
- package/bin/sync-assets.test.mjs +64 -0
- package/mcp/azurepr-mcp-bridge/README.md +37 -0
- package/mcp/azurepr-mcp-bridge/azure-devops.js +327 -0
- package/mcp/azurepr-mcp-bridge/config.toml.example +7 -0
- package/mcp/azurepr-mcp-bridge/index.js +65 -0
- package/mcp/azurepr-mcp-bridge/index.test.js +116 -0
- package/mcp/azurepr-mcp-bridge/package.json +22 -0
- package/mcp/rag-mcp-bridge/README.md +42 -0
- package/mcp/rag-mcp-bridge/codex-system-prompt.md +20 -0
- package/mcp/rag-mcp-bridge/config.toml.example +12 -0
- package/mcp/rag-mcp-bridge/index.js +361 -0
- package/mcp/rag-mcp-bridge/index.test.js +56 -0
- package/mcp/rag-mcp-bridge/package.json +21 -0
- package/package.json +44 -0
- package/plugins/autobest-delivery/.codex-plugin/plugin.json +25 -0
- package/plugins/autobest-delivery/.mcp.json +11 -0
- package/plugins/autobest-delivery/README.md +164 -0
- package/plugins/autobest-delivery/assets/delivery-report-template.xlsx +0 -0
- package/plugins/autobest-delivery/mcp-server/npm-shrinkwrap.json +3511 -0
- package/plugins/autobest-delivery/mcp-server/package.json +23 -0
- package/plugins/autobest-delivery/mcp-server/src/paths.mjs +43 -0
- package/plugins/autobest-delivery/mcp-server/src/report.mjs +605 -0
- package/plugins/autobest-delivery/mcp-server/src/runner.mjs +489 -0
- package/plugins/autobest-delivery/mcp-server/src/server.mjs +199 -0
- package/plugins/autobest-delivery/mcp-server/tests/fixture-server.mjs +36 -0
- package/plugins/autobest-delivery/mcp-server/tests/fixtures/basic.feature.mjs +68 -0
- package/plugins/autobest-delivery/mcp-server/tests/mcp-smoke.test.mjs +83 -0
- package/plugins/autobest-delivery/mcp-server/tests/report.test.mjs +254 -0
- package/plugins/autobest-delivery/mcp-server/tests/runner.test.mjs +354 -0
- package/plugins/autobest-delivery/scripts/export-delivery-report.mjs +41 -0
- package/plugins/autobest-delivery/scripts/setup.mjs +295 -0
- package/plugins/autobest-delivery/scripts/setup.test.mjs +145 -0
- package/plugins/autobest-delivery/scripts/start-mcp.mjs +7 -0
- package/plugins/autobest-delivery/skills/code-audit/SKILL.md +24 -0
- package/plugins/autobest-delivery/skills/code-audit/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/code-craft/SKILL.md +27 -0
- package/plugins/autobest-delivery/skills/code-craft/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/delivery-loop/SKILL.md +43 -0
- package/plugins/autobest-delivery/skills/delivery-loop/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/delivery-loop/references/delivery-contract.md +235 -0
- package/plugins/autobest-delivery/skills/e2e-gen-spec/SKILL.md +35 -0
- package/plugins/autobest-delivery/skills/e2e-gen-spec/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/e2e-ui-checker/SKILL.md +30 -0
- package/plugins/autobest-delivery/skills/e2e-ui-checker/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/export-report/SKILL.md +66 -0
- package/plugins/autobest-delivery/skills/export-report/agents/openai.yaml +8 -0
- package/plugins/autobest-delivery/skills/ui-structure-guard/SKILL.md +24 -0
- package/plugins/autobest-delivery/skills/ui-structure-guard/agents/openai.yaml +7 -0
- package/skills/README.md +38 -0
- package/skills/common/figma-ui-capture/SKILL.md +197 -0
- package/skills/common/figma-ui-capture/agents/openai.yaml +4 -0
- package/skills/common/ui-prd-scope/SKILL.md +67 -0
- package/skills/common/ui-prd-scope/agents/openai.yaml +4 -0
- package/skills/common/ui-prd-scope/references/scope-schema.md +158 -0
- package/skills/common/ui-prd-scope/scripts/validate-scope-bundle.mjs +302 -0
- package/skills/react/react-code-standards/SKILL.md +78 -0
- package/skills/react/react-code-standards/agents/openai.yaml +4 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { PathPolicyError, resolveWorkspacePaths } from '../src/paths.mjs';
|
|
8
|
+
import {
|
|
9
|
+
activeBlockedRunCount,
|
|
10
|
+
checkEnvironment,
|
|
11
|
+
resolveBlockedRun,
|
|
12
|
+
runFeatureE2E
|
|
13
|
+
} from '../src/runner.mjs';
|
|
14
|
+
import { startFixtureServer } from './fixture-server.mjs';
|
|
15
|
+
|
|
16
|
+
const testsRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const fixtureScenario = path.join(testsRoot, 'fixtures', 'basic.feature.mjs');
|
|
18
|
+
|
|
19
|
+
async function createWorkspace() {
|
|
20
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'autobest-delivery-'));
|
|
21
|
+
const scenarioPath = path.join(root, 'feature', 'e2e', 'e2e.feature.mjs');
|
|
22
|
+
await fs.mkdir(path.dirname(scenarioPath), { recursive: true });
|
|
23
|
+
await fs.copyFile(fixtureScenario, scenarioPath);
|
|
24
|
+
return { root, scenarioPath };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('checkEnvironment 能启动插件自带浏览器', async () => {
|
|
28
|
+
const result = await checkEnvironment();
|
|
29
|
+
assert.equal(result.ready, true, result.error);
|
|
30
|
+
assert.match(result.browserVersion, /^\d+\./);
|
|
31
|
+
assert.match(result.browserStorage, /autobest-delivery/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('checkEnvironment 能报告浏览器运行时缺失', async () => {
|
|
35
|
+
const result = await checkEnvironment({
|
|
36
|
+
load: async () => {
|
|
37
|
+
throw new Error('缺少浏览器可执行文件');
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
assert.equal(result.ready, false);
|
|
41
|
+
assert.match(result.error, /缺少浏览器可执行文件/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('runFeatureE2E 能执行冻结场景并写入可审查证据', async t => {
|
|
45
|
+
const fixture = await startFixtureServer();
|
|
46
|
+
t.after(fixture.close);
|
|
47
|
+
const workspace = await createWorkspace();
|
|
48
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
49
|
+
const before = await fs.readFile(workspace.scenarioPath, 'utf8');
|
|
50
|
+
const protectedFiles = {
|
|
51
|
+
'package.json': '{"private":true}\n',
|
|
52
|
+
'yarn.lock': '# sentinel\n',
|
|
53
|
+
'config/build.json': '{"sentinel":true}\n',
|
|
54
|
+
'node_modules/sentinel.txt': 'unchanged\n'
|
|
55
|
+
};
|
|
56
|
+
for (const [relativePath, content] of Object.entries(protectedFiles)) {
|
|
57
|
+
const target = path.join(workspace.root, relativePath);
|
|
58
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
59
|
+
await fs.writeFile(target, content, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = await runFeatureE2E({
|
|
63
|
+
workspaceRoot: workspace.root,
|
|
64
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
65
|
+
baseUrl: fixture.baseUrl,
|
|
66
|
+
outputDir: 'feature/e2e/runs/iteration-01',
|
|
67
|
+
iteration: 1,
|
|
68
|
+
timeoutMs: 15000
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
assert.equal(result.status, 'passed');
|
|
72
|
+
assert.equal(result.checks.length, 2);
|
|
73
|
+
assert.equal(result.checks.every(item => item.isPass), true);
|
|
74
|
+
assert.deepEqual(
|
|
75
|
+
result.checks.map(item => [
|
|
76
|
+
item.reportModule,
|
|
77
|
+
item.reportGroup,
|
|
78
|
+
item.reportTitle,
|
|
79
|
+
item.reportMethod
|
|
80
|
+
]),
|
|
81
|
+
[
|
|
82
|
+
['运行器测试页', 'fixture-heading', '页面标题', '检查页面标题是否正确显示。'],
|
|
83
|
+
['运行器测试页', 'fixture-result', '运行结果', '检查操作后的运行结果是否正确显示。']
|
|
84
|
+
]
|
|
85
|
+
);
|
|
86
|
+
assert.equal(result.captures.length, 1);
|
|
87
|
+
assert.equal(await fs.readFile(workspace.scenarioPath, 'utf8'), before);
|
|
88
|
+
for (const [relativePath, content] of Object.entries(protectedFiles)) {
|
|
89
|
+
assert.equal(await fs.readFile(path.join(workspace.root, relativePath), 'utf8'), content);
|
|
90
|
+
}
|
|
91
|
+
await fs.access(path.join(workspace.root, result.resultPath));
|
|
92
|
+
await fs.access(path.join(workspace.root, result.tracePath));
|
|
93
|
+
await fs.access(path.join(workspace.root, result.captures[0]));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('报告 schema v1 拒绝缺少通用分组字段的检查', async t => {
|
|
97
|
+
const workspace = await createWorkspace();
|
|
98
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
99
|
+
await fs.writeFile(
|
|
100
|
+
workspace.scenarioPath,
|
|
101
|
+
`export const metadata = { reportSchemaVersion: 1, visualMappings: [] };
|
|
102
|
+
export default async function ({ check }) {
|
|
103
|
+
await check({
|
|
104
|
+
id: 'missing-report-group',
|
|
105
|
+
specSnippet: '检查必须声明报告分组。',
|
|
106
|
+
scene: 'initial',
|
|
107
|
+
errorType: '功能缺陷',
|
|
108
|
+
expect: '分组字段完整'
|
|
109
|
+
}, async () => '不应执行');
|
|
110
|
+
}\n`,
|
|
111
|
+
'utf8'
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const result = await runFeatureE2E({
|
|
115
|
+
workspaceRoot: workspace.root,
|
|
116
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
117
|
+
baseUrl: 'http://127.0.0.1:1',
|
|
118
|
+
outputDir: 'feature/e2e/runs/missing-report-group',
|
|
119
|
+
timeoutMs: 10000
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
assert.equal(result.status, 'blocked');
|
|
123
|
+
assert.match(result.blockers[0].detail, /reportModule/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('runFeatureE2E 将已完成的断言失败报告为 failed', async t => {
|
|
127
|
+
const fixture = await startFixtureServer();
|
|
128
|
+
t.after(fixture.close);
|
|
129
|
+
const workspace = await createWorkspace();
|
|
130
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
131
|
+
await fs.writeFile(
|
|
132
|
+
workspace.scenarioPath,
|
|
133
|
+
`export default async function ({ page, expect, check, baseUrl }) {
|
|
134
|
+
await page.goto(baseUrl);
|
|
135
|
+
await check({
|
|
136
|
+
id: 'expected-failure',
|
|
137
|
+
specSnippet: '缺少内容时检查应失败。',
|
|
138
|
+
scene: 'initial',
|
|
139
|
+
errorType: '功能缺陷',
|
|
140
|
+
expect: '不存在的内容可见'
|
|
141
|
+
}, async () => expect(page.getByText('不存在的内容')).toBeVisible());
|
|
142
|
+
}\n`,
|
|
143
|
+
'utf8'
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const result = await runFeatureE2E({
|
|
147
|
+
workspaceRoot: workspace.root,
|
|
148
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
149
|
+
baseUrl: fixture.baseUrl,
|
|
150
|
+
outputDir: 'feature/e2e/runs/failed',
|
|
151
|
+
timeoutMs: 10000
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
assert.equal(result.status, 'failed');
|
|
155
|
+
assert.equal(result.checks[0].isPass, false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('runFeatureE2E 在隔离 scene 阻塞后继续执行后续 scene', async t => {
|
|
159
|
+
const fixture = await startFixtureServer();
|
|
160
|
+
t.after(fixture.close);
|
|
161
|
+
const workspace = await createWorkspace();
|
|
162
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
163
|
+
await fs.writeFile(
|
|
164
|
+
workspace.scenarioPath,
|
|
165
|
+
`export const metadata = { scenes: ['blocked-selection', 'later-content'] };
|
|
166
|
+
export default async function ({ scene }) {
|
|
167
|
+
await scene('blocked-selection', async ({ page, baseUrl }) => {
|
|
168
|
+
await page.goto(baseUrl);
|
|
169
|
+
throw new Error('Year 2020 option not found');
|
|
170
|
+
});
|
|
171
|
+
await scene('later-content', async ({ page, expect, check, baseUrl }) => {
|
|
172
|
+
await page.goto(baseUrl);
|
|
173
|
+
await check({
|
|
174
|
+
id: 'later-heading',
|
|
175
|
+
specSnippet: '后续独立场景仍应执行。',
|
|
176
|
+
scene: 'later-content',
|
|
177
|
+
errorType: '功能缺陷',
|
|
178
|
+
expect: '后续标题可见'
|
|
179
|
+
}, async () => {
|
|
180
|
+
await expect(page.getByRole('heading', { name: '交付测试页' })).toBeVisible();
|
|
181
|
+
return '后续场景已执行';
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}\n`,
|
|
185
|
+
'utf8'
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const result = await runFeatureE2E({
|
|
189
|
+
workspaceRoot: workspace.root,
|
|
190
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
191
|
+
baseUrl: fixture.baseUrl,
|
|
192
|
+
outputDir: 'feature/e2e/runs/isolated-scenes',
|
|
193
|
+
timeoutMs: 10000
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
assert.equal(result.status, 'blocked');
|
|
197
|
+
assert.equal(result.blockers.length, 1);
|
|
198
|
+
assert.equal(result.blockers[0].kind, 'scenario');
|
|
199
|
+
assert.equal(result.blockers[0].scene, 'blocked-selection');
|
|
200
|
+
assert.deepEqual(result.checks.map(item => item.id), ['later-heading']);
|
|
201
|
+
assert.equal(result.checks[0].isPass, true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('runFeatureE2E 在 scene 尚未记录检查时保留原始 blocker', async t => {
|
|
205
|
+
const workspace = await createWorkspace();
|
|
206
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
207
|
+
await fs.writeFile(
|
|
208
|
+
workspace.scenarioPath,
|
|
209
|
+
`export const metadata = { scenes: ['year-selection'] };
|
|
210
|
+
export default async function ({ scene }) {
|
|
211
|
+
await scene('year-selection', async () => {
|
|
212
|
+
throw new Error('Year 2020 option not found');
|
|
213
|
+
});
|
|
214
|
+
}\n`,
|
|
215
|
+
'utf8'
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const result = await runFeatureE2E({
|
|
219
|
+
workspaceRoot: workspace.root,
|
|
220
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
221
|
+
baseUrl: 'http://127.0.0.1:1',
|
|
222
|
+
outputDir: 'feature/e2e/runs/blocked-before-check',
|
|
223
|
+
timeoutMs: 10000
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
assert.equal(result.status, 'blocked');
|
|
227
|
+
assert.equal(result.checks.length, 0);
|
|
228
|
+
assert.deepEqual(result.blockers, [{
|
|
229
|
+
kind: 'scenario',
|
|
230
|
+
scene: 'year-selection',
|
|
231
|
+
detail: 'Year 2020 option not found'
|
|
232
|
+
}]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('runFeatureE2E 将无效场景语法报告为 blocked', async t => {
|
|
236
|
+
const workspace = await createWorkspace();
|
|
237
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
238
|
+
await fs.writeFile(workspace.scenarioPath, 'export default function ( {', 'utf8');
|
|
239
|
+
|
|
240
|
+
const result = await runFeatureE2E({
|
|
241
|
+
workspaceRoot: workspace.root,
|
|
242
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
243
|
+
baseUrl: 'http://127.0.0.1:1',
|
|
244
|
+
outputDir: 'feature/e2e/runs/invalid',
|
|
245
|
+
timeoutMs: 1000
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
assert.equal(result.status, 'blocked');
|
|
249
|
+
assert.equal(result.blockers[0].kind, 'scenario');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('runFeatureE2E 拒绝包导入和 Node 全局变量', async t => {
|
|
253
|
+
const workspace = await createWorkspace();
|
|
254
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
255
|
+
await fs.writeFile(
|
|
256
|
+
workspace.scenarioPath,
|
|
257
|
+
"import fs from 'node:fs';\nexport default async function () { return fs; }\n",
|
|
258
|
+
'utf8'
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const result = await runFeatureE2E({
|
|
262
|
+
workspaceRoot: workspace.root,
|
|
263
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
264
|
+
baseUrl: 'http://127.0.0.1:1',
|
|
265
|
+
outputDir: 'feature/e2e/runs/import',
|
|
266
|
+
timeoutMs: 1000
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
assert.equal(result.status, 'blocked');
|
|
270
|
+
assert.match(result.blockers[0].detail, /不得使用 import/);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('runFeatureE2E 超时后关闭浏览器并保持可复用', async t => {
|
|
274
|
+
const workspace = await createWorkspace();
|
|
275
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
276
|
+
await fs.writeFile(
|
|
277
|
+
workspace.scenarioPath,
|
|
278
|
+
'export default async function () { await new Promise(() => {}); }\n',
|
|
279
|
+
'utf8'
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const result = await runFeatureE2E({
|
|
283
|
+
workspaceRoot: workspace.root,
|
|
284
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
285
|
+
baseUrl: 'http://127.0.0.1:1',
|
|
286
|
+
outputDir: 'feature/e2e/runs/timeout',
|
|
287
|
+
timeoutMs: 100
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
assert.equal(result.status, 'blocked');
|
|
291
|
+
assert.equal(result.blockers[0].kind, 'environment');
|
|
292
|
+
assert.match(result.blockers[0].detail, /超时/);
|
|
293
|
+
assert.equal((await checkEnvironment()).ready, true);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('Blocked 运行可保留浏览器并记录人工 skip 决议', async t => {
|
|
297
|
+
const fixture = await startFixtureServer();
|
|
298
|
+
t.after(fixture.close);
|
|
299
|
+
const workspace = await createWorkspace();
|
|
300
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
301
|
+
await fs.writeFile(
|
|
302
|
+
workspace.scenarioPath,
|
|
303
|
+
`export default async function ({ page, baseUrl }) {
|
|
304
|
+
await page.goto(baseUrl);
|
|
305
|
+
throw new Error('等待人工确认');
|
|
306
|
+
}\n`,
|
|
307
|
+
'utf8'
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const result = await runFeatureE2E({
|
|
311
|
+
workspaceRoot: workspace.root,
|
|
312
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
313
|
+
baseUrl: fixture.baseUrl,
|
|
314
|
+
outputDir: 'feature/e2e/runs/human-decision',
|
|
315
|
+
timeoutMs: 10000,
|
|
316
|
+
headed: false,
|
|
317
|
+
keepBrowserOpenOnBlock: true,
|
|
318
|
+
blockedSessionTtlMs: 60000
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
assert.equal(result.status, 'blocked');
|
|
322
|
+
assert.match(result.blockedSessionId, /^[0-9a-f-]{36}$/);
|
|
323
|
+
assert.equal(result.blockedSessionExpiresAt !== null, true);
|
|
324
|
+
assert.equal(activeBlockedRunCount(), 1);
|
|
325
|
+
|
|
326
|
+
const decision = await resolveBlockedRun({
|
|
327
|
+
sessionId: result.blockedSessionId,
|
|
328
|
+
decision: 'skip',
|
|
329
|
+
note: '用户接受未完成的 E2E 证据。'
|
|
330
|
+
});
|
|
331
|
+
assert.equal(decision.status, 'resolved');
|
|
332
|
+
assert.equal(decision.effect, 'continue-with-waiver');
|
|
333
|
+
assert.equal(activeBlockedRunCount(), 0);
|
|
334
|
+
|
|
335
|
+
const recorded = JSON.parse(
|
|
336
|
+
await fs.readFile(path.join(workspace.root, decision.decisionPath), 'utf8')
|
|
337
|
+
);
|
|
338
|
+
assert.equal(recorded.decision, 'skip');
|
|
339
|
+
assert.equal(recorded.runnerResult, result.resultPath);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('工作区策略拒绝场景和输出路径越界', async t => {
|
|
343
|
+
const workspace = await createWorkspace();
|
|
344
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
345
|
+
|
|
346
|
+
await assert.rejects(
|
|
347
|
+
resolveWorkspacePaths({
|
|
348
|
+
workspaceRoot: workspace.root,
|
|
349
|
+
scenarioPath: workspace.scenarioPath,
|
|
350
|
+
outputDir: '../outside'
|
|
351
|
+
}),
|
|
352
|
+
PathPolicyError
|
|
353
|
+
);
|
|
354
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { exportDeliveryReport } from '../mcp-server/src/report.mjs';
|
|
4
|
+
|
|
5
|
+
function usage() {
|
|
6
|
+
return '用法:node scripts/export-delivery-report.mjs <功能目录> [--output <功能目录内的 xlsx 路径>] [--workspace-root <工作区>] [--grouping <功能目录内的语义分组 JSON>]';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const values = [...argv];
|
|
11
|
+
const featureDir = values.shift();
|
|
12
|
+
if (!featureDir || featureDir.startsWith('--')) {
|
|
13
|
+
throw new Error(usage());
|
|
14
|
+
}
|
|
15
|
+
const options = { featureDir };
|
|
16
|
+
while (values.length > 0) {
|
|
17
|
+
const flag = values.shift();
|
|
18
|
+
const value = values.shift();
|
|
19
|
+
if (!value) {
|
|
20
|
+
throw new Error(`${flag} 缺少参数\n${usage()}`);
|
|
21
|
+
}
|
|
22
|
+
if (flag === '--output') {
|
|
23
|
+
options.outputPath = value;
|
|
24
|
+
} else if (flag === '--workspace-root') {
|
|
25
|
+
options.workspaceRoot = value;
|
|
26
|
+
} else if (flag === '--grouping') {
|
|
27
|
+
options.groupingPath = value;
|
|
28
|
+
} else {
|
|
29
|
+
throw new Error(`未知参数:${flag}\n${usage()}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return options;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await exportDeliveryReport(parseArgs(process.argv.slice(2)));
|
|
37
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
process.stderr.write(`${error.message}\n`);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { cp, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const PLUGIN_NAME = 'autobest-delivery';
|
|
10
|
+
const MARKETPLACE_NAME = 'autobest-team';
|
|
11
|
+
const PLUGIN_SELECTOR = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
12
|
+
const sourcePluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const packageRoot = path.resolve(sourcePluginRoot, '..', '..');
|
|
14
|
+
const agentHome = path.resolve(
|
|
15
|
+
process.env.AUTOBEST_AGENT_HOME || path.join(os.homedir(), '.autobest-agent')
|
|
16
|
+
);
|
|
17
|
+
const marketplaceRoot = path.join(agentHome, 'marketplaces', MARKETPLACE_NAME);
|
|
18
|
+
const marketplacePath = path.join(marketplaceRoot, '.agents', 'plugins', 'marketplace.json');
|
|
19
|
+
const installedPluginRoot = path.join(marketplaceRoot, 'plugins', PLUGIN_NAME);
|
|
20
|
+
const npmCommand = process.env.AUTOBEST_AGENT_NPM_COMMAND ||
|
|
21
|
+
(process.platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
22
|
+
const codexCommand = process.env.AUTOBEST_AGENT_CODEX_COMMAND ||
|
|
23
|
+
(process.platform === 'win32' ? 'codex.exe' : 'codex');
|
|
24
|
+
|
|
25
|
+
const marketplace = {
|
|
26
|
+
name: MARKETPLACE_NAME,
|
|
27
|
+
interface: {
|
|
28
|
+
displayName: 'Autobest Team'
|
|
29
|
+
},
|
|
30
|
+
plugins: [
|
|
31
|
+
{
|
|
32
|
+
name: PLUGIN_NAME,
|
|
33
|
+
source: {
|
|
34
|
+
source: 'local',
|
|
35
|
+
path: `./plugins/${PLUGIN_NAME}`
|
|
36
|
+
},
|
|
37
|
+
policy: {
|
|
38
|
+
installation: 'AVAILABLE',
|
|
39
|
+
authentication: 'ON_INSTALL'
|
|
40
|
+
},
|
|
41
|
+
category: 'Productivity'
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function printHelp() {
|
|
47
|
+
process.stdout.write(`Autobest Delivery 插件安装器
|
|
48
|
+
|
|
49
|
+
用法:
|
|
50
|
+
autobest-delivery-setup [install]
|
|
51
|
+
autobest-delivery-setup uninstall
|
|
52
|
+
autobest-delivery-setup --help
|
|
53
|
+
|
|
54
|
+
命令:
|
|
55
|
+
install 安装或更新插件(默认)
|
|
56
|
+
uninstall 卸载插件并删除 Autobest 专用市场目录
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function run(command, args, options = {}) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const child = spawn(command, args, {
|
|
63
|
+
env: process.env,
|
|
64
|
+
stdio: 'inherit',
|
|
65
|
+
...options
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
child.once('error', error => {
|
|
69
|
+
reject(new Error(`无法执行 ${command}:${error.message}`, { cause: error }));
|
|
70
|
+
});
|
|
71
|
+
child.once('exit', code => {
|
|
72
|
+
if (code === 0) {
|
|
73
|
+
resolve();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
reject(new Error(`${command} ${args.join(' ')} 执行失败,状态码为 ${code}`));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function runJson(command, args) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const child = spawn(command, args, {
|
|
84
|
+
env: process.env,
|
|
85
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
86
|
+
});
|
|
87
|
+
let stdout = '';
|
|
88
|
+
let stderr = '';
|
|
89
|
+
|
|
90
|
+
child.stdout.setEncoding('utf8');
|
|
91
|
+
child.stderr.setEncoding('utf8');
|
|
92
|
+
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
93
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
94
|
+
child.once('error', error => {
|
|
95
|
+
reject(new Error(`无法执行 ${command}:${error.message}`, { cause: error }));
|
|
96
|
+
});
|
|
97
|
+
child.once('exit', code => {
|
|
98
|
+
if (code !== 0) {
|
|
99
|
+
reject(new Error(
|
|
100
|
+
`${command} ${args.join(' ')} 执行失败,状态码为 ${code}${stderr ? `\n${stderr.trim()}` : ''}`
|
|
101
|
+
));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
resolve(JSON.parse(stdout));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
reject(new Error(`${command} 未返回有效 JSON,请升级 Codex CLI。`, { cause: error }));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isExcludedSourcePath(source) {
|
|
114
|
+
const relative = path.relative(sourcePluginRoot, source);
|
|
115
|
+
if (!relative) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return relative.split(path.sep).some(part => part === 'node_modules' || part === '.runtime');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function readPackageVersion() {
|
|
122
|
+
const packageJson = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
123
|
+
if (typeof packageJson.version !== 'string' || !packageJson.version) {
|
|
124
|
+
throw new Error('npm 包 package.json 缺少有效的 version。');
|
|
125
|
+
}
|
|
126
|
+
return packageJson.version;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function preparePlugin() {
|
|
130
|
+
const stagingParent = path.join(agentHome, '.staging');
|
|
131
|
+
await mkdir(stagingParent, { recursive: true });
|
|
132
|
+
const stagingRoot = await mkdtemp(path.join(stagingParent, `${PLUGIN_NAME}-`));
|
|
133
|
+
const stagedPluginRoot = path.join(stagingRoot, PLUGIN_NAME);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await cp(sourcePluginRoot, stagedPluginRoot, {
|
|
137
|
+
recursive: true,
|
|
138
|
+
filter: source => !isExcludedSourcePath(source)
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const manifestPath = path.join(stagedPluginRoot, '.codex-plugin', 'plugin.json');
|
|
142
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
143
|
+
manifest.version = `${await readPackageVersion()}+codex.npm`;
|
|
144
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
145
|
+
|
|
146
|
+
const serverRoot = path.join(stagedPluginRoot, 'mcp-server');
|
|
147
|
+
await run(npmCommand, ['ci'], { cwd: serverRoot });
|
|
148
|
+
|
|
149
|
+
const playwrightCommand = path.join(
|
|
150
|
+
serverRoot,
|
|
151
|
+
'node_modules',
|
|
152
|
+
'.bin',
|
|
153
|
+
process.platform === 'win32' ? 'playwright.cmd' : 'playwright'
|
|
154
|
+
);
|
|
155
|
+
const browserRoot = path.join(serverRoot, '.runtime', 'ms-playwright');
|
|
156
|
+
const existingBrowserRoot = path.join(
|
|
157
|
+
installedPluginRoot,
|
|
158
|
+
'mcp-server',
|
|
159
|
+
'.runtime',
|
|
160
|
+
'ms-playwright'
|
|
161
|
+
);
|
|
162
|
+
try {
|
|
163
|
+
await cp(existingBrowserRoot, browserRoot, { recursive: true });
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (!error || error.code !== 'ENOENT') {
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
await run(playwrightCommand, ['install', 'chromium'], {
|
|
170
|
+
cwd: serverRoot,
|
|
171
|
+
env: {
|
|
172
|
+
...process.env,
|
|
173
|
+
PLAYWRIGHT_BROWSERS_PATH: browserRoot
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return { stagingRoot, stagedPluginRoot };
|
|
178
|
+
} catch (error) {
|
|
179
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function replaceInstalledPlugin(stagedPluginRoot, stagingRoot) {
|
|
185
|
+
await mkdir(path.dirname(installedPluginRoot), { recursive: true });
|
|
186
|
+
const backupRoot = `${installedPluginRoot}.previous`;
|
|
187
|
+
await rm(backupRoot, { recursive: true, force: true });
|
|
188
|
+
|
|
189
|
+
let hasBackup = false;
|
|
190
|
+
try {
|
|
191
|
+
await rename(installedPluginRoot, backupRoot);
|
|
192
|
+
hasBackup = true;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (!error || error.code !== 'ENOENT') {
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await rename(stagedPluginRoot, installedPluginRoot);
|
|
201
|
+
await rm(backupRoot, { recursive: true, force: true });
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (hasBackup) {
|
|
204
|
+
await rename(backupRoot, installedPluginRoot);
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
} finally {
|
|
208
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function writeMarketplace() {
|
|
213
|
+
await mkdir(path.dirname(marketplacePath), { recursive: true });
|
|
214
|
+
await writeFile(marketplacePath, `${JSON.stringify(marketplace, null, 2)}\n`, 'utf8');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function getCodexState() {
|
|
218
|
+
const [marketplacesResult, pluginsResult] = await Promise.all([
|
|
219
|
+
runJson(codexCommand, ['plugin', 'marketplace', 'list', '--json']),
|
|
220
|
+
runJson(codexCommand, ['plugin', 'list', '--json'])
|
|
221
|
+
]);
|
|
222
|
+
return {
|
|
223
|
+
marketplaces: Array.isArray(marketplacesResult.marketplaces)
|
|
224
|
+
? marketplacesResult.marketplaces
|
|
225
|
+
: [],
|
|
226
|
+
installed: Array.isArray(pluginsResult.installed) ? pluginsResult.installed : []
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function configureCodex() {
|
|
231
|
+
const state = await getCodexState();
|
|
232
|
+
const installed = state.installed.some(plugin => plugin.pluginId === PLUGIN_SELECTOR);
|
|
233
|
+
const configuredMarketplace = state.marketplaces.find(item => item.name === MARKETPLACE_NAME);
|
|
234
|
+
|
|
235
|
+
if (installed) {
|
|
236
|
+
await run(codexCommand, ['plugin', 'remove', PLUGIN_SELECTOR, '--json']);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (configuredMarketplace && path.resolve(configuredMarketplace.root) !== marketplaceRoot) {
|
|
240
|
+
await run(codexCommand, ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME, '--json']);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!configuredMarketplace || path.resolve(configuredMarketplace.root) !== marketplaceRoot) {
|
|
244
|
+
await run(codexCommand, ['plugin', 'marketplace', 'add', marketplaceRoot, '--json']);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await run(codexCommand, ['plugin', 'add', PLUGIN_SELECTOR, '--json']);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function install() {
|
|
251
|
+
process.stdout.write('正在准备 Autobest Delivery 独立运行时,这可能需要几分钟...\n');
|
|
252
|
+
const prepared = await preparePlugin();
|
|
253
|
+
await replaceInstalledPlugin(prepared.stagedPluginRoot, prepared.stagingRoot);
|
|
254
|
+
await writeMarketplace();
|
|
255
|
+
await configureCodex();
|
|
256
|
+
process.stdout.write(`Autobest Delivery 插件已安装:${installedPluginRoot}\n`);
|
|
257
|
+
process.stdout.write('请重启 Codex,并在新会话中使用插件。\n');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function assertSafeMarketplaceRoot() {
|
|
261
|
+
const expected = path.join(agentHome, 'marketplaces', MARKETPLACE_NAME);
|
|
262
|
+
if (marketplaceRoot !== expected || path.basename(marketplaceRoot) !== MARKETPLACE_NAME) {
|
|
263
|
+
throw new Error(`拒绝删除非预期路径:${marketplaceRoot}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function uninstall() {
|
|
268
|
+
const state = await getCodexState();
|
|
269
|
+
if (state.installed.some(plugin => plugin.pluginId === PLUGIN_SELECTOR)) {
|
|
270
|
+
await run(codexCommand, ['plugin', 'remove', PLUGIN_SELECTOR, '--json']);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const configuredMarketplace = state.marketplaces.find(item => item.name === MARKETPLACE_NAME);
|
|
274
|
+
if (configuredMarketplace && path.resolve(configuredMarketplace.root) === marketplaceRoot) {
|
|
275
|
+
await run(codexCommand, ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME, '--json']);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
assertSafeMarketplaceRoot();
|
|
279
|
+
await rm(marketplaceRoot, { recursive: true, force: true });
|
|
280
|
+
process.stdout.write('Autobest Delivery 插件及其专用本地市场已卸载。\n');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const command = process.argv[2] || 'install';
|
|
284
|
+
|
|
285
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
286
|
+
printHelp();
|
|
287
|
+
} else if (command === 'install') {
|
|
288
|
+
await install();
|
|
289
|
+
} else if (command === 'uninstall') {
|
|
290
|
+
await uninstall();
|
|
291
|
+
} else {
|
|
292
|
+
process.stderr.write(`未知命令:${command}\n\n`);
|
|
293
|
+
printHelp();
|
|
294
|
+
process.exitCode = 1;
|
|
295
|
+
}
|