@danceiny/gotry 0.2.0-rc.29 → 0.2.0-rc.31
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/gotry-bootstrap.js +56 -83
- package/bin/gotry-extension-status.d.ts +12 -0
- package/bin/gotry-extension-status.js +113 -0
- package/bin/gotry-flyai-verification.d.ts +9 -0
- package/bin/gotry-flyai-verification.js +63 -0
- package/bin/gotry-inner.js +9 -2
- package/client/client.js +183 -0
- package/cordis.gotry-patch.yml +2 -0
- package/dist/capabilities/artifacts.js +41 -4
- package/dist/capabilities/doctor.js +7 -15
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +2 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +135 -2
- package/dist/scripts/artifact-client-contract-tests.js +4 -3
- package/dist/scripts/artifact-delivery-tests.js +31 -0
- package/dist/scripts/bootstrap-tests.js +19 -4
- package/dist/scripts/doctor-tests.js +36 -7
- package/dist/scripts/dsh-artifact-web-e2e.js +204 -43
- package/dist/scripts/flyai-setup-tests.js +75 -2
- package/dist/scripts/gotry-web-api-tests.js +114 -0
- package/dist/scripts/gotry-web-client-tests.js +28 -0
- package/dist/scripts/issue-271-liveness-tests.js +9 -1
- package/dist/scripts/turn-handoff-collect-tests.js +7 -1
- package/dist/scripts/turn-handoff-collect.js +68 -20
- package/dist/scripts/turn-handoff-job-tests.js +222 -0
- package/dist/scripts/turn-handoff-web-e2e.js +240 -0
- package/dist/src/artifact-delivery.js +26 -0
- package/dist/src/gotry-web-api.js +157 -0
- package/dist/src/index.js +30 -8
- package/dist/src/turn-deadline.js +113 -16
- package/dist/src/turn-handoff-job.js +124 -0
- package/extension/manifest.json +2 -2
- package/package.json +8 -2
- package/ts/capabilities/artifacts.ts +40 -5
- package/ts/scripts/turn-handoff-collect.ts +63 -21
- package/ts/src/artifact-delivery.ts +25 -0
- package/ts/src/gotry-web-api.ts +140 -0
- package/ts/src/index.ts +30 -8
- package/ts/src/turn-deadline.ts +111 -18
- package/ts/src/turn-handoff-job.ts +120 -0
|
@@ -21,9 +21,14 @@ import { EventEmitter } from 'node:events';
|
|
|
21
21
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
22
22
|
import { tmpdir } from 'node:os';
|
|
23
23
|
import { join, dirname, delimiter } from 'node:path';
|
|
24
|
-
import {
|
|
24
|
+
import { pathToFileURL } from 'node:url';
|
|
25
|
+
import { runDoctorChecks as diagnose } from '../capabilities/doctor.js';
|
|
25
26
|
const repoRoot = join(import.meta.dirname, '..', '..');
|
|
26
27
|
const bootstrap = join(repoRoot, 'bin', 'gotry-bootstrap.js');
|
|
28
|
+
const offlinePreload = pathToFileURL(join(repoRoot, 'scripts', 'doctor-test-isolation.mjs')).href;
|
|
29
|
+
process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ''} --import=${offlinePreload}`.trim();
|
|
30
|
+
process.env.GOTRY_TEST_DOCTOR_PORT = '0';
|
|
31
|
+
const runDoctorChecks = (opts = {}) => diagnose({ ...opts, extensionPorts: [] });
|
|
27
32
|
function runBootstrap(extraArgs, extraEnv, bootstrapPath = bootstrap) {
|
|
28
33
|
try {
|
|
29
34
|
const out = execFileSync('node', [bootstrapPath, ...extraArgs], {
|
|
@@ -156,7 +161,7 @@ console.log('9. calendar 子命令(setup 状态面 on/off/status + doctor 三态
|
|
|
156
161
|
assert.ok(!r.stdout.includes('[gotry-doctor]'), 'summary 模式零 header(stdout 静默)');
|
|
157
162
|
assert.match(r.stderr, /\[gotry\] doctor: \d+ 项待处理/, '隔离 HOME 有缺失项 → 一行摘要进 stderr');
|
|
158
163
|
assert.match(r.stderr, /gotry_doctor/, '摘要带对话内指路');
|
|
159
|
-
assert.match(r.stderr,
|
|
164
|
+
assert.match(r.stderr, /扩展=待确认/, '没有安装或连接证据时,摘要显示待确认');
|
|
160
165
|
}
|
|
161
166
|
console.log('10. 启动一次性 doctor 摘要(--summary:stderr 一行/零写盘/恒 exit 0)OK');
|
|
162
167
|
// 11. setupSidebar 落盘状态复核(2026-09-08 安装链修复):pnpm ≥10.5 在 profile 目录
|
|
@@ -388,6 +393,9 @@ console.log('18. onboarding CLI 跳过(非 TTY / CI / GOTRY_SETUP_SKIP / GOTRY_O
|
|
|
388
393
|
const fixture = mkdtempSync(join(tmpdir(), 'gotry-onboard-pkg-'));
|
|
389
394
|
mkdirSync(join(fixture, 'bin'), { recursive: true });
|
|
390
395
|
copyFileSync(bootstrap, join(fixture, 'bin', 'gotry-bootstrap.js'));
|
|
396
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-flyai-verification.js'), join(fixture, 'bin', 'gotry-flyai-verification.js'));
|
|
397
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-process-liveness.js'), join(fixture, 'bin', 'gotry-process-liveness.js'));
|
|
398
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-extension-status.js'), join(fixture, 'bin', 'gotry-extension-status.js'));
|
|
391
399
|
copyFileSync(join(repoRoot, 'package.json'), join(fixture, 'package.json'));
|
|
392
400
|
const vendorDir = join(fixture, 'ts', 'dsh-runtime', 'vendor');
|
|
393
401
|
mkdirSync(vendorDir, { recursive: true });
|
|
@@ -629,9 +637,16 @@ function buildPkgFixture(tmpRoot) {
|
|
|
629
637
|
mkdirSync(pkgBin, { recursive: true });
|
|
630
638
|
mkdirSync(pkgDist, { recursive: true });
|
|
631
639
|
mkdirSync(join(dshDir, 'lib'), { recursive: true });
|
|
640
|
+
// The real patch now resolves native present before invoking this fake DSH.
|
|
641
|
+
const presentDir = join(pkgRoot, 'node_modules', '@deepseek-ai', 'dsh-tool-present');
|
|
642
|
+
mkdirSync(presentDir, { recursive: true });
|
|
643
|
+
writeFileSync(join(presentDir, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-tool-present', main: 'index.js' }));
|
|
644
|
+
writeFileSync(join(presentDir, 'index.js'), 'module.exports = {}\n');
|
|
632
645
|
writeFileSync(join(pkgRoot, 'package.json'), `${JSON.stringify({ name: 'gotry', version: '0.0.1-test', type: 'module' })}\n`);
|
|
633
646
|
copyFileSync(join(repoRoot, 'bin', 'gotry-inner.js'), join(pkgBin, 'gotry-inner.js'));
|
|
634
647
|
copyFileSync(join(repoRoot, 'bin', 'gotry-bootstrap.js'), join(pkgBin, 'gotry-bootstrap.js'));
|
|
648
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-flyai-verification.js'), join(pkgBin, 'gotry-flyai-verification.js'));
|
|
649
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-extension-status.js'), join(pkgBin, 'gotry-extension-status.js'));
|
|
635
650
|
copyFileSync(join(repoRoot, 'bin', 'gotry-runtime-resolution.js'), join(pkgBin, 'gotry-runtime-resolution.js'));
|
|
636
651
|
copyFileSync(join(repoRoot, 'bin', 'gotry-process-liveness.js'), join(pkgBin, 'gotry-process-liveness.js'));
|
|
637
652
|
copyFileSync(join(repoRoot, 'cordis.gotry-patch.yml'), join(pkgRoot, 'cordis.gotry-patch.yml'));
|
|
@@ -694,8 +709,8 @@ echo "bash-finished $$" >> "$GOTRY_TEST_INSTALLER_MARKER"
|
|
|
694
709
|
}
|
|
695
710
|
const closedFixtures = new WeakSet();
|
|
696
711
|
function spawnInner(fixture, opts) {
|
|
697
|
-
//
|
|
698
|
-
const nodeOptions = opts.tty ?
|
|
712
|
+
// 保留离线诊断 preload,但移除父进程 tsx loader;临时包没有 tsx。
|
|
713
|
+
const nodeOptions = `--import=${offlinePreload}${opts.tty ? ` --require ${fixture.preloadPath}` : ''}`;
|
|
699
714
|
const child = spawn(process.execPath, [join(fixture.pkgBin, 'gotry-inner.js'), 'web', '--no-open'], {
|
|
700
715
|
env: {
|
|
701
716
|
...process.env,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* doctor 能力层测试(离线,注入 repoRoot/homeDir/env,零安装零网络写路径):
|
|
3
|
-
* 1. 空 tmp 环境 → agent-reach=missing / 扩展=
|
|
3
|
+
* 1. 空 tmp 环境 → agent-reach=missing / 扩展=degraded(连接未确认) / flyai=degraded(无 key)/ LLM key 恒 ok 且不进 broken
|
|
4
4
|
* 2. 补齐假 .venv 双文件 → agent-reach=ok;只补 python → degraded(半可用态被显式区分)
|
|
5
5
|
* 3. FLYAI_API_KEY 仅注入 → flyai=degraded(未验证);匹配验证回执 → flyai=ok
|
|
6
6
|
* 4. nodeOk 边界(22.14/22.15/23.0)
|
|
@@ -25,9 +25,13 @@ import { existsSync } from 'node:fs';
|
|
|
25
25
|
import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
|
|
26
26
|
import { tmpdir } from 'node:os';
|
|
27
27
|
import { dirname, join } from 'node:path';
|
|
28
|
-
import { runDoctorChecks, renderDoctorReportMd, nodeOk, probeStdout, scopeKeyFor, createRepairApprovalGate, runDoctorRepair } from '../capabilities/doctor.js';
|
|
28
|
+
import { runDoctorChecks as diagnose, renderDoctorReportMd, nodeOk, probeStdout, scopeKeyFor, createRepairApprovalGate, runDoctorRepair } from '../capabilities/doctor.js';
|
|
29
29
|
import { endpointFingerprint, maskFlyaiKey, resolveFlyaiEndpoint, sha256Hex, writeFlyaiVerification } from '../capabilities/flyai-config.js';
|
|
30
30
|
import { apply } from '../src/index.js';
|
|
31
|
+
const runDoctorChecks = (opts = {}) => diagnose({ ...opts, extensionPorts: [] });
|
|
32
|
+
// The registered model tool calls diagnose internally rather than this wrapper.
|
|
33
|
+
// Isolate its HTTP diagnostic seam too, without changing the product config.
|
|
34
|
+
await import(new URL('../../scripts/doctor-test-isolation.mjs', import.meta.url).href);
|
|
31
35
|
const tmp = await mkdtemp(join(tmpdir(), 'gotry-doctor-test-'));
|
|
32
36
|
const emptyRepo = join(tmp, 'repo-empty');
|
|
33
37
|
const emptyHome = join(tmp, 'home-empty');
|
|
@@ -39,11 +43,10 @@ const byId = (id) => r1.items.find(i => i.id === id);
|
|
|
39
43
|
assert.ok(byId('agent-reach'), 'agent-reach 项存在');
|
|
40
44
|
assert.equal(byId('agent-reach').status, 'missing', `空仓 agent-reach=missing,实际 ${byId('agent-reach').status}`);
|
|
41
45
|
assert.match(byId('agent-reach').fix ?? '', /npx @danceiny\/gotry doctor --fix/, 'missing 项带精确补装指引');
|
|
42
|
-
assert.equal(byId('extension').status, '
|
|
43
|
-
|
|
44
|
-
assert.match(byId('extension').
|
|
45
|
-
assert.match(byId('extension').detail,
|
|
46
|
-
assert.match(byId('extension').detail, /其它工具/, '扩展 missing detail 必显「其它不受影响」');
|
|
46
|
+
assert.equal(byId('extension').status, 'degraded', '空 home 无法断言扩展未安装');
|
|
47
|
+
assert.match(byId('extension').detail, /影响面/, '扩展 detail 必说明影响面');
|
|
48
|
+
assert.match(byId('extension').fix ?? '', /chrome:\/\/extensions/, '扩展未确认时可检查浏览器安装/启用状态');
|
|
49
|
+
assert.match(byId('extension').detail, /其它工具/, '扩展 detail 必说明其它工具不受影响');
|
|
47
50
|
assert.equal(byId('flyai').status, 'degraded', '无 key flyai=degraded(有共享试用,非缺失)');
|
|
48
51
|
// #559 B 步:无 key flyai 必须显式说明「匿名共享额度」影响面
|
|
49
52
|
assert.match(byId('flyai').detail, /影响面/, 'flyai 无 key detail 必带影响面行(issue #559 B 步)');
|
|
@@ -155,6 +158,32 @@ console.log('5b. npm 提升布局解析链(map-tools/ask-user 误报修复)OK');
|
|
|
155
158
|
await rm(vend, { recursive: true, force: true });
|
|
156
159
|
}
|
|
157
160
|
console.log('5c. 随包 vendor 布局(map-tools tarball 分发面)OK');
|
|
161
|
+
// #651: the model-facing diagnostic must recognize the same Chrome Store
|
|
162
|
+
// installation as the CLI without requiring the local unpacked directory.
|
|
163
|
+
{
|
|
164
|
+
const home = join(tmp, 'chrome-store-home');
|
|
165
|
+
const browserRoot = process.platform === 'darwin'
|
|
166
|
+
? join(home, 'Library', 'Application Support', 'Google', 'Chrome')
|
|
167
|
+
: process.platform === 'win32'
|
|
168
|
+
? join(home, 'AppData', 'Local', 'Google', 'Chrome', 'User Data')
|
|
169
|
+
: join(home, '.config', 'google-chrome');
|
|
170
|
+
const profile = join(browserRoot, 'Profile 3');
|
|
171
|
+
const id = 'oeajpiccmonococjcegddlooeeohlbgd';
|
|
172
|
+
const manifest = join(profile, 'Extensions', id, '0.2.0.27_0', 'manifest.json');
|
|
173
|
+
await mkdir(dirname(manifest), { recursive: true });
|
|
174
|
+
await writeFile(manifest, JSON.stringify({ manifest_version: 3, version: '0.2.0.27', name: 'Stai Travel Bridge' }));
|
|
175
|
+
const prefs = join(profile, 'Secure Preferences');
|
|
176
|
+
await writeFile(prefs, JSON.stringify({ extensions: { settings: { [id]: { disable_reasons: [] } } } }));
|
|
177
|
+
const report = await runDoctorChecks({ repoRoot: emptyRepo, homeDir: home, env: {} });
|
|
178
|
+
const extension = report.items.find(item => item.id === 'extension');
|
|
179
|
+
assert.equal(extension.status, 'degraded');
|
|
180
|
+
assert.match(extension.detail, /已安装/);
|
|
181
|
+
assert.match(report.summary, /Stai Travel Bridge 扩展\(待确认\)/);
|
|
182
|
+
await writeFile(prefs, JSON.stringify({ extensions: { settings: { [id]: { state: 0 } } } }));
|
|
183
|
+
const disabled = (await runDoctorChecks({ repoRoot: emptyRepo, homeDir: home, env: {} })).items.find(item => item.id === 'extension');
|
|
184
|
+
assert.match(disabled.detail, /已安装.*停用/);
|
|
185
|
+
}
|
|
186
|
+
console.log('5d. Chrome 商店安装与停用状态,工具层不误报 missing/ready OK');
|
|
158
187
|
// 6. MCP 工具面:gotry_doctor 注册 + isolated stateRoot 报告落盘
|
|
159
188
|
const smokeRoot = await mkdtemp(join(tmpdir(), 'gotry-doctor-state-'));
|
|
160
189
|
const registered = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fresh-profile, installed-package Web E2E for GoTry artifact cards.
|
|
3
3
|
*
|
|
4
|
-
* The only model endpoint is a local
|
|
4
|
+
* The only model endpoint is a local Chat/Messages SSE relay. The test
|
|
5
5
|
* starts `gotry web --no-open --port 0`, captures the authenticated `dsh web:`
|
|
6
6
|
* URL, then drives the real composer with a fresh headless Chrome profile.
|
|
7
7
|
* It verifies the public Client cards, path selection, the normal read → edit
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import assert from 'node:assert/strict';
|
|
13
13
|
import { createHash } from 'node:crypto';
|
|
14
14
|
import { createServer } from 'node:http';
|
|
15
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
16
16
|
import { tmpdir } from 'node:os';
|
|
17
17
|
import { basename, join } from 'node:path';
|
|
18
18
|
import { spawn } from 'node:child_process';
|
|
@@ -63,21 +63,38 @@ function toolNames(body) {
|
|
|
63
63
|
}).filter(Boolean);
|
|
64
64
|
}
|
|
65
65
|
function hasToolMessage(body) {
|
|
66
|
-
return (body.messages ?? []).some(message => message.role === 'tool'
|
|
66
|
+
return (body.messages ?? []).some(message => message.role === 'tool'
|
|
67
|
+
|| Array.isArray(message.content) && message.content.some(block => block?.type === 'tool_result'));
|
|
67
68
|
}
|
|
68
69
|
function lastUserText(body) {
|
|
69
|
-
const message = [...(body.messages ?? [])].reverse().find(item => item.role === 'user'
|
|
70
|
+
const message = [...(body.messages ?? [])].reverse().find(item => item.role === 'user'
|
|
71
|
+
&& (!Array.isArray(item.content) || item.content.some(block => block?.type === 'text')));
|
|
70
72
|
const content = message?.content;
|
|
71
73
|
if (typeof content === 'string')
|
|
72
74
|
return content;
|
|
73
75
|
return JSON.stringify(content ?? '');
|
|
74
76
|
}
|
|
77
|
+
/** Current DSH uses DeepSeek Messages; keep the relay compatible with both wires. */
|
|
78
|
+
function messagesResponse(chatPayload) {
|
|
79
|
+
const first = JSON.parse(chatPayload.split('\n').find(line => line.startsWith('data: ')).slice(6));
|
|
80
|
+
const delta = first.choices[0].delta, call = delta.tool_calls?.[0];
|
|
81
|
+
const event = (type, value) => `event: ${type}\ndata: ${JSON.stringify({ type, ...value })}\n\n`;
|
|
82
|
+
return event('message_start', { message: { id: first.id, type: 'message', role: 'assistant', model: first.model,
|
|
83
|
+
content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 1, output_tokens: 0 } } })
|
|
84
|
+
+ event('content_block_start', { index: 0, content_block: call
|
|
85
|
+
? { type: 'tool_use', id: call.id, name: call.function.name, input: {} } : { type: 'text', text: '' } })
|
|
86
|
+
+ event('content_block_delta', { index: 0, delta: call
|
|
87
|
+
? { type: 'input_json_delta', partial_json: call.function.arguments } : { type: 'text_delta', text: delta.content || '' } })
|
|
88
|
+
+ event('content_block_stop', { index: 0 })
|
|
89
|
+
+ event('message_delta', { delta: { stop_reason: call ? 'tool_use' : 'end_turn', stop_sequence: null }, usage: { output_tokens: 1 } })
|
|
90
|
+
+ event('message_stop', {});
|
|
91
|
+
}
|
|
75
92
|
async function startRelay() {
|
|
76
93
|
const bodies = [];
|
|
77
94
|
const servedTools = [];
|
|
78
95
|
let phase = 'first-list';
|
|
79
96
|
const server = createServer((req, res) => {
|
|
80
|
-
if (req.method !== 'POST' ||
|
|
97
|
+
if (req.method !== 'POST' || !/\/(chat\/completions|messages)$/.test(req.url || '')) {
|
|
81
98
|
res.writeHead(404).end('not found');
|
|
82
99
|
return;
|
|
83
100
|
}
|
|
@@ -141,12 +158,26 @@ async function startRelay() {
|
|
|
141
158
|
payload = toolResponse('artifact-third-read', 'gotry_artifacts_read', { path: 'trip-2027.html' });
|
|
142
159
|
}
|
|
143
160
|
else if (phase === 'third-final' && hasToolMessage(body)) {
|
|
161
|
+
phase = 'fourth-render';
|
|
144
162
|
payload = textResponse('artifact-third-final', '已读取 HTML 产物,请使用列表中的「Open HTML preview」按钮验证原生预览。');
|
|
145
163
|
}
|
|
164
|
+
else if (phase === 'fourth-render' && lastUserText(body).includes('生成')) {
|
|
165
|
+
phase = 'fourth-final';
|
|
166
|
+
servedTools.push('gotry_itinerary_render');
|
|
167
|
+
payload = toolResponse('artifact-fourth-render', 'gotry_itinerary_render', {
|
|
168
|
+
title: 'Native delivery fixture', basename: 'gotry-itinerary-native-delivery.html', fact_ids: [],
|
|
169
|
+
itinerary: { trip_start: '2027-04-01', trip_end: '2027-04-02',
|
|
170
|
+
stays: [{ place: 'Shanghai', check_in: '2027-04-01', check_out: '2027-04-02' }], od_segments: [] },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else if (phase === 'fourth-final' && hasToolMessage(body)) {
|
|
174
|
+
phase = 'done';
|
|
175
|
+
payload = textResponse('artifact-fourth-final', '原生文件交付已生成;本计划未核验。');
|
|
176
|
+
}
|
|
146
177
|
else {
|
|
147
178
|
payload = textResponse(`unexpected-${bodies.length}`, '');
|
|
148
179
|
}
|
|
149
|
-
res.end(payload);
|
|
180
|
+
res.end(req.url?.endsWith('/messages') ? messagesResponse(payload) : payload);
|
|
150
181
|
});
|
|
151
182
|
});
|
|
152
183
|
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
@@ -189,6 +220,14 @@ async function main() {
|
|
|
189
220
|
const homeDir = mkdtempSync(join(tmpdir(), 'gotry-artifact-web-home-'));
|
|
190
221
|
const dshHome = mkdtempSync(join(tmpdir(), 'gotry-artifact-web-dsh-'));
|
|
191
222
|
const userDataDir = mkdtempSync(join(tmpdir(), 'gotry-artifact-web-chrome-'));
|
|
223
|
+
const verifier = join(homeDir, 'flyai-verifier.cjs');
|
|
224
|
+
writeFileSync(verifier, `#!/usr/bin/env node
|
|
225
|
+
if (process.env.FLYAI_API_KEY === 'sk-web-bad-candidate') {
|
|
226
|
+
process.stderr.write('HTTP 401 ' + process.env.FLYAI_API_KEY); process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
process.stdout.write(JSON.stringify({data:{itemList:[]}}));
|
|
229
|
+
`);
|
|
230
|
+
chmodSync(verifier, 0o700);
|
|
192
231
|
const fixture = '# Trip 2027\n\nDay 1: arrival\nDay 2: city walk\nDay 3: museum\nDay 4: rest\nDay 5: return\n';
|
|
193
232
|
writeFileSync(join(workspaceDir, 'trip-2027.md'), fixture, { mode: 0o600 });
|
|
194
233
|
const htmlSentinel = 'gotry-artifact-web-html-sentinel-must-not-run-in-host';
|
|
@@ -216,6 +255,7 @@ document.documentElement.setAttribute('data-${htmlSentinel}', 'host-ran-it');
|
|
|
216
255
|
let server = null;
|
|
217
256
|
let passed = false;
|
|
218
257
|
let serverOutput = '';
|
|
258
|
+
const browserErrors = [];
|
|
219
259
|
const assertions = {
|
|
220
260
|
isolation: { home: homeDir, dshHome, workspace: workspaceDir, userDataDir, liveFlags: { GOTRY_SESSION_LIVE: '0', GOTRY_HBCLI_LIVE: '0', GOTRY_HOTELBYTE_SKILLS_LIVE: '0' } },
|
|
221
261
|
};
|
|
@@ -225,7 +265,8 @@ document.documentElement.setAttribute('data-${htmlSentinel}', 'host-ran-it');
|
|
|
225
265
|
const tarball = join(packageDir, readdirSync(packageDir).find(name => name.endsWith('.tgz')) || '');
|
|
226
266
|
assert.ok(existsSync(tarball), 'npm pack did not produce a GoTry tarball');
|
|
227
267
|
const packageJson = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
|
|
228
|
-
await run('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], { cwd: consumerDir,
|
|
268
|
+
await run('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], { cwd: consumerDir,
|
|
269
|
+
env: { ...process.env, HOME: homeDir, npm_config_cache: join(outputDir, 'npm-cache') } });
|
|
229
270
|
const installedRoot = join(consumerDir, 'node_modules', '@danceiny', 'gotry');
|
|
230
271
|
const installedBin = join(installedRoot, 'bin', 'gotry-inner.js');
|
|
231
272
|
assert.ok(existsSync(installedBin), 'packed GoTry binary missing');
|
|
@@ -244,7 +285,15 @@ document.documentElement.setAttribute('data-${htmlSentinel}', 'host-ran-it');
|
|
|
244
285
|
LLM_API_KEY: 'synthetic-artifact-web-key',
|
|
245
286
|
LLM_BASE_URL: `http://127.0.0.1:${relay.port}/v1`,
|
|
246
287
|
LLM_MODEL: 'synthetic-artifact-web',
|
|
288
|
+
DEEPSEEK_API_KEY: 'synthetic-artifact-web-key',
|
|
289
|
+
DEEPSEEK_BASE_URL: `http://127.0.0.1:${relay.port}/v1`,
|
|
290
|
+
GOTRY_FLYAI_CLI_BIN: verifier,
|
|
291
|
+
GOTRY_FLYAI_VERIFY_TIMEOUT_MS: '5000',
|
|
292
|
+
GOTRY_TURN_HANDOFF_ROOT: workspaceDir,
|
|
247
293
|
};
|
|
294
|
+
delete env.FLYAI_API_KEY;
|
|
295
|
+
delete env.DEBUG_FLYAI_API_KEY;
|
|
296
|
+
delete env.DEBUG_FLYAI_MCP_URL;
|
|
248
297
|
const child = spawn(process.execPath, [installedBin, 'web', '--no-open', '--port', '0'], { cwd: workspaceDir, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
249
298
|
server = child;
|
|
250
299
|
child.stdout.on('data', chunk => { serverOutput += chunk.toString(); });
|
|
@@ -255,14 +304,15 @@ document.documentElement.setAttribute('data-${htmlSentinel}', 'host-ran-it');
|
|
|
255
304
|
browser = await puppeteer.launch({ executablePath: CHROME, headless: true, userDataDir, args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'] });
|
|
256
305
|
page = await browser.newPage();
|
|
257
306
|
page.on('console', (message) => { serverOutput += `\n[browser console ${message.type()}] ${message.text()}`; });
|
|
258
|
-
page.on('pageerror', (error) => { serverOutput += `\n[browser pageerror] ${error.stack || error.message}`; });
|
|
307
|
+
page.on('pageerror', (error) => { browserErrors.push(error.message); serverOutput += `\n[browser pageerror] ${error.stack || error.message}`; });
|
|
308
|
+
await page.setViewport({ width: 1440, height: 1000 });
|
|
259
309
|
page.setDefaultTimeout(30_000);
|
|
260
310
|
await page.goto(urlResult.url, { waitUntil: 'networkidle2', timeout: 30_000 });
|
|
261
311
|
// A fresh DSH profile may show the product's first-run disclosure before
|
|
262
312
|
// the composer is mounted. Dismiss that visible UI gate through its real
|
|
263
313
|
// button; do not seed cookies or attach an existing browser profile.
|
|
264
314
|
await page.evaluate(() => {
|
|
265
|
-
const button = [...document.querySelectorAll('button')].find(node => node.textContent?.trim()
|
|
315
|
+
const button = [...document.querySelectorAll('button')].find(node => /^(继续|Continue)$/.test(node.textContent?.trim() || ''));
|
|
266
316
|
button?.click();
|
|
267
317
|
});
|
|
268
318
|
// Register the isolated fixture through DSH's authenticated public RPC
|
|
@@ -291,25 +341,28 @@ document.documentElement.setAttribute('data-${htmlSentinel}', 'host-ran-it');
|
|
|
291
341
|
// host-authoritative registration through its normal baseline path.
|
|
292
342
|
await page.reload({ waitUntil: 'networkidle2', timeout: 30_000 });
|
|
293
343
|
await page.evaluate(() => {
|
|
294
|
-
const button = [...document.querySelectorAll('button')].find(node => node.textContent?.trim()
|
|
344
|
+
const button = [...document.querySelectorAll('button')].find(node => /^(继续|Continue)$/.test(node.textContent?.trim() || ''));
|
|
295
345
|
button?.click();
|
|
296
346
|
});
|
|
297
347
|
await page.waitForFunction((title) => document.body.innerText.includes(title), { timeout: 30_000 }, workspaceTitle);
|
|
298
|
-
await page.click('button[aria-label="选择工作区"], button[aria-label="Select workspace"]');
|
|
299
|
-
await page.waitForFunction((title) => [...document.querySelectorAll('[role="menuitem"], [role="option"]
|
|
348
|
+
await page.click('button[aria-label="选择工作区"], button[aria-label="Choose workspace"], button[aria-label="Select workspace"]');
|
|
349
|
+
await page.waitForFunction((title) => [...document.querySelectorAll('[role="menuitem"], [role="option"]')].some(node => node.textContent?.trim() === title), { timeout: 30_000 }, workspaceTitle);
|
|
300
350
|
await page.evaluate((title) => {
|
|
301
|
-
const node = [...document.querySelectorAll('[role="menuitem"], [role="option"]
|
|
351
|
+
const node = [...document.querySelectorAll('[role="menuitem"], [role="option"]')].find(candidate => candidate.textContent?.trim() === title);
|
|
302
352
|
node?.click();
|
|
303
353
|
}, workspaceTitle);
|
|
354
|
+
await page.waitForFunction((title) => [...document.querySelectorAll('button[aria-label="选择工作区"], button[aria-label="Choose workspace"], button[aria-label="Select workspace"]')]
|
|
355
|
+
.some(node => node.textContent?.includes(title)), { timeout: 30_000 }, workspaceTitle);
|
|
304
356
|
await page.waitForSelector('[contenteditable="true"]');
|
|
357
|
+
console.log('Web E2E: isolated installed package and selected workspace ready');
|
|
305
358
|
const submit = async (text) => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
await
|
|
359
|
+
await page.locator('[contenteditable="true"]').fill(text);
|
|
360
|
+
await page.waitForFunction(() => {
|
|
361
|
+
const button = document.querySelector('button[aria-label="发送消息"], button[aria-label="Send message"]');
|
|
362
|
+
return button && !button.disabled;
|
|
363
|
+
});
|
|
364
|
+
await page.locator('button[aria-label="发送消息"], button[aria-label="Send message"]').click();
|
|
365
|
+
await page.waitForFunction((message) => document.body.innerText.includes(message), { timeout: 30_000 }, text);
|
|
313
366
|
};
|
|
314
367
|
await submit('请列出并打开刚才生成的 trip-2027.md 行程产物。');
|
|
315
368
|
const revealArtifactCalls = async () => {
|
|
@@ -319,19 +372,29 @@ document.documentElement.setAttribute('data-${htmlSentinel}', 'host-ran-it');
|
|
|
319
372
|
process.click();
|
|
320
373
|
}
|
|
321
374
|
});
|
|
375
|
+
await page.waitForFunction(() => [...document.querySelectorAll('[data-turn-process]')]
|
|
376
|
+
.every(node => node.disabled || node.getAttribute('aria-expanded') === 'true'));
|
|
377
|
+
await page.evaluate(() => {
|
|
378
|
+
for (const activity of document.querySelectorAll('[data-process-activity]')) {
|
|
379
|
+
if (activity.getAttribute('aria-expanded') !== 'true')
|
|
380
|
+
activity.click();
|
|
381
|
+
}
|
|
382
|
+
});
|
|
322
383
|
};
|
|
323
384
|
await page.waitForFunction(() => document.querySelector('[data-turn-process="1"]') !== null, { timeout: 60_000 });
|
|
385
|
+
await page.waitForFunction(() => document.body.innerText.includes('已展示行程产物。'), { timeout: 60_000 });
|
|
324
386
|
await revealArtifactCalls();
|
|
325
387
|
await page.waitForSelector('[data-gotry-artifact-card="list"]');
|
|
326
388
|
await page.waitForSelector('[data-gotry-artifact-card="list"][data-gotry-artifact-state="ok"]');
|
|
327
389
|
await page.waitForSelector('[data-gotry-artifact-card="read"] [data-gotry-artifact-line="1"]');
|
|
390
|
+
console.log('Web E2E: native tool cards visible');
|
|
328
391
|
assert.equal(await page.$eval('[data-gotry-artifact-card="list"]', (node) => {
|
|
329
392
|
const rect = node.getBoundingClientRect();
|
|
330
|
-
return rect.width > 0 && rect.height > 0;
|
|
393
|
+
return !node.closest('[hidden]') && rect.width > 0 && rect.height > 0;
|
|
331
394
|
}), true, 'artifact list card is not visibly rendered');
|
|
332
395
|
assert.equal(await page.$eval('[data-gotry-artifact-card="read"]', (node) => {
|
|
333
396
|
const rect = node.getBoundingClientRect();
|
|
334
|
-
return rect.width > 0 && rect.height > 0;
|
|
397
|
+
return !node.closest('[hidden]') && rect.width > 0 && rect.height > 0;
|
|
335
398
|
}), true, 'artifact read card is not visibly rendered');
|
|
336
399
|
const pathButton = await page.$('[data-gotry-artifact-path$="trip-2027.md"]');
|
|
337
400
|
assert.ok(pathButton, 'list card path button missing');
|
|
@@ -353,14 +416,15 @@ document.documentElement.setAttribute('data-${htmlSentinel}', 'host-ran-it');
|
|
|
353
416
|
assertions.firstTurn = firstSnapshot;
|
|
354
417
|
await submit('请修改刚才的行程产物,在末尾追加 Day 6,然后重新读取并展示更新版本。');
|
|
355
418
|
await page.waitForFunction(() => document.querySelector('[data-turn-process="2"]') !== null, { timeout: 60_000 });
|
|
419
|
+
await page.waitForFunction(() => document.body.innerText.includes('已更新并重新读取行程产物。'), { timeout: 60_000 });
|
|
356
420
|
await revealArtifactCalls();
|
|
357
421
|
await page.waitForFunction(() => document.querySelectorAll('[data-gotry-artifact-card="read"]').length >= 2, { timeout: 60_000 });
|
|
358
422
|
await page.waitForFunction(() => [...document.querySelectorAll('[data-gotry-artifact-card="read"]')].some(node => node.textContent?.includes('Day 6: revision requested')), { timeout: 60_000 });
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
// the
|
|
362
|
-
|
|
363
|
-
|
|
423
|
+
// Independently verify the native file preview. The current Host may
|
|
424
|
+
// refresh automatically; a retained older page must expose a reload
|
|
425
|
+
// control. In either case the rendered sidebar must show the new line.
|
|
426
|
+
await page.waitForFunction(() => document.querySelector('[data-textpreview-changed]')
|
|
427
|
+
|| document.querySelector('[data-textpreview-body]')?.textContent?.includes('Day 6: revision requested'), { timeout: 60_000 });
|
|
364
428
|
const staleRightPreview = await page.$eval('[data-textpreview-state="text"]', (node) => {
|
|
365
429
|
const rect = node.getBoundingClientRect();
|
|
366
430
|
const body = node.querySelector('[data-textpreview-body]');
|
|
@@ -371,21 +435,19 @@ document.documentElement.setAttribute('data-${htmlSentinel}', 'host-ran-it');
|
|
|
371
435
|
};
|
|
372
436
|
});
|
|
373
437
|
assert.equal(staleRightPreview.visible, true, 'right file preview is not visibly rendered before reload');
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
visible: rect.width > 0 && rect.height > 0,
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
assert.match(reloadControl.text, /重新载入|Reload/);
|
|
388
|
-
await reloadButton.click();
|
|
438
|
+
let reloadControl = { visible: false, text: 'Automatic native refresh', ariaLabel: null };
|
|
439
|
+
if (!staleRightPreview.includesDay6) {
|
|
440
|
+
assert.equal(staleRightPreview.changedBanner, true, 'a stale file preview must expose its changed-file banner');
|
|
441
|
+
const reloadButton = await page.$('[data-textpreview-changed] [data-textpreview-reload-now]');
|
|
442
|
+
assert.ok(reloadButton, 'right file preview reload control missing');
|
|
443
|
+
reloadControl = await page.$eval('[data-textpreview-changed] [data-textpreview-reload-now]', (node) => {
|
|
444
|
+
const rect = node.getBoundingClientRect();
|
|
445
|
+
return { visible: rect.width > 0 && rect.height > 0, text: node.textContent?.trim() || '', ariaLabel: node.getAttribute('aria-label') };
|
|
446
|
+
});
|
|
447
|
+
assert.equal(reloadControl.visible, true, 'right file preview reload control is not visible');
|
|
448
|
+
assert.match(reloadControl.text, /重新载入|Reload/);
|
|
449
|
+
await reloadButton.click();
|
|
450
|
+
}
|
|
389
451
|
await page.waitForFunction(() => {
|
|
390
452
|
const preview = document.querySelector('[data-textpreview-state="text"]');
|
|
391
453
|
const body = preview?.querySelector('[data-textpreview-body]');
|
|
@@ -477,6 +539,7 @@ document.documentElement.setAttribute('data-${htmlSentinel}', 'host-ran-it');
|
|
|
477
539
|
// terms, not against hidden/animating geometry.
|
|
478
540
|
await submit('请用 gotry_artifacts_list 列出当前工作区里 HTML 产物,然后用 gotry_artifacts_read 读取 trip-2027.html 的源码展示给用户。');
|
|
479
541
|
await page.waitForFunction(() => document.querySelector('[data-turn-process="3"]') !== null, { timeout: 60_000 });
|
|
542
|
+
await page.waitForFunction(() => document.body.innerText.includes('已读取 HTML 产物'), { timeout: 60_000 });
|
|
480
543
|
await revealArtifactCalls();
|
|
481
544
|
await page.waitForFunction(() => [...document.querySelectorAll('[data-gotry-artifact-card="read"]')].some(node => node.getAttribute('data-gotry-artifact-source') === 'html'), { timeout: 60_000 });
|
|
482
545
|
const htmlSourceSnapshot = await page.evaluate((sentinel) => {
|
|
@@ -628,6 +691,7 @@ document.documentElement.setAttribute('data-${htmlSentinel}', 'host-ran-it');
|
|
|
628
691
|
screenshots: { page: htmlScreenshotPath, iframe: htmlIframeScreenshotPath },
|
|
629
692
|
htmlFixture: { path: join(workspaceDir, 'trip-2027.html'), sentinel: htmlSentinel },
|
|
630
693
|
};
|
|
694
|
+
console.log('Web E2E: read/edit/refresh and native HTML sandbox passed');
|
|
631
695
|
assert.deepEqual(relay.servedTools, [
|
|
632
696
|
'gotry_artifacts_list',
|
|
633
697
|
'gotry_artifacts_read',
|
|
@@ -637,6 +701,102 @@ document.documentElement.setAttribute('data-${htmlSentinel}', 'host-ran-it');
|
|
|
637
701
|
'gotry_artifacts_list',
|
|
638
702
|
'gotry_artifacts_read',
|
|
639
703
|
]);
|
|
704
|
+
// The render tool emits the same durable presentation event as native
|
|
705
|
+
// present. No extra model call or invented Client-only card is involved.
|
|
706
|
+
await submit('请生成一个未核验的 HTML 行程文件,验证原生文件交付。');
|
|
707
|
+
await page.waitForFunction(() => document.body.innerText.includes('原生文件交付已生成'), { timeout: 60_000 });
|
|
708
|
+
await page.waitForSelector('[data-presented-file] button[title$="gotry-itinerary-native-delivery.html"]');
|
|
709
|
+
assert.ok(existsSync(join(workspaceDir, 'gotry-itinerary-native-delivery.html')));
|
|
710
|
+
await page.click('[data-presented-file] button[title$="gotry-itinerary-native-delivery.html"]');
|
|
711
|
+
const generatedFrame = await page.waitForFrame(async (frame) => {
|
|
712
|
+
if (!frame.url().startsWith('blob:'))
|
|
713
|
+
return false;
|
|
714
|
+
return frame.evaluate(() => document.querySelector('h1')?.textContent === 'Native delivery fixture').catch(() => false);
|
|
715
|
+
}, { timeout: 30_000 });
|
|
716
|
+
assert.ok(generatedFrame, 'native delivery card did not open the generated file');
|
|
717
|
+
await page.screenshot({ path: join(outputDir, 'gotry-native-delivery.png'), fullPage: false });
|
|
718
|
+
assertions.delivery = { nativeCard: true, nativePreview: true, filename: 'gotry-itinerary-native-delivery.html' };
|
|
719
|
+
// Search and paginate the persistent workbench without asking the model.
|
|
720
|
+
for (let index = 0; index < 22; index++)
|
|
721
|
+
writeFileSync(join(workspaceDir, `history-${String(index).padStart(2, '0')}.md`), '# History fixture\n');
|
|
722
|
+
const modelRequestsBeforeBrowse = relay.bodies.length;
|
|
723
|
+
const clickText = async (pattern, root = 'body') => {
|
|
724
|
+
await page.waitForFunction((source, selector) => [...document.querySelectorAll(selector + ' button')].some(node => {
|
|
725
|
+
const rect = node.getBoundingClientRect();
|
|
726
|
+
return rect.width > 0 && rect.height > 0 && !node.disabled
|
|
727
|
+
&& new RegExp(source).test(node.textContent?.trim() || node.getAttribute('aria-label') || '');
|
|
728
|
+
}), { timeout: 30_000 }, pattern, root);
|
|
729
|
+
await page.evaluate((source, selector) => {
|
|
730
|
+
const node = [...document.querySelectorAll(selector + ' button')].find(node => {
|
|
731
|
+
const rect = node.getBoundingClientRect();
|
|
732
|
+
return rect.width > 0 && rect.height > 0 && !node.disabled
|
|
733
|
+
&& new RegExp(source).test(node.textContent?.trim() || node.getAttribute('aria-label') || '');
|
|
734
|
+
});
|
|
735
|
+
node.click();
|
|
736
|
+
}, pattern, root);
|
|
737
|
+
};
|
|
738
|
+
await page.click('button[aria-label="新标签页"], button[aria-label="New tab"]');
|
|
739
|
+
await clickText('^GoTry (产物|artifacts)', '[data-sidebar-right-guide]');
|
|
740
|
+
await page.waitForSelector('[data-gotry-artifact-entry]');
|
|
741
|
+
assert.equal(await page.$$eval('[data-gotry-artifact-entry]', (nodes) => nodes.length), 20);
|
|
742
|
+
await clickText('^(下一页|Next)$', '[data-gotry-artifacts-browser]');
|
|
743
|
+
await page.waitForFunction(() => document.querySelector('[data-gotry-artifacts-browser] [role="status"]')?.textContent?.startsWith('21–'));
|
|
744
|
+
const search = '[data-gotry-artifacts-browser] input';
|
|
745
|
+
await page.type(search, 'trip-2027');
|
|
746
|
+
await page.waitForFunction(() => document.querySelectorAll('[data-gotry-artifact-entry]').length === 2);
|
|
747
|
+
await page.screenshot({ path: join(outputDir, 'gotry-artifacts-workbench.png'), fullPage: false });
|
|
748
|
+
await page.click('[data-gotry-artifact-entry$="trip-2027.html"]');
|
|
749
|
+
const historyFrame = await page.waitForFrame(async (frame) => {
|
|
750
|
+
if (!frame.url().startsWith('blob:'))
|
|
751
|
+
return false;
|
|
752
|
+
return frame.evaluate(() => document.querySelector('#artifact-web-h1')?.textContent === 'GoTry artifact-web visible heading').catch(() => false);
|
|
753
|
+
}, { timeout: 30_000 });
|
|
754
|
+
assert.ok(historyFrame, 'persistent workbench failed to open HTML in the native preview');
|
|
755
|
+
assert.equal(relay.bodies.length, modelRequestsBeforeBrowse, 'history browsing must not invoke the model');
|
|
756
|
+
assertions.workbench = { entryVisible: true, pageSize: 20, pagination: true, searchMatches: 2, nativePreview: true, modelRequests: 0 };
|
|
757
|
+
// Native Plugins navigation and real official-file persistence.
|
|
758
|
+
await clickText('^(插件|Plugins)$');
|
|
759
|
+
await page.waitForSelector('[data-plugin-item="gotry"] button');
|
|
760
|
+
await page.click('[data-plugin-item="gotry"] button');
|
|
761
|
+
await page.waitForFunction(() => /匿名试用|Anonymous trial/.test(document.querySelector('[data-gotry-flyai-status]')?.textContent || ''));
|
|
762
|
+
assert.equal(await page.$eval('#gotry-flyai-key', (node) => node.type), 'password');
|
|
763
|
+
const goodKey = 'sk-web-good-candidate-1234', badKey = 'sk-web-bad-candidate';
|
|
764
|
+
await page.type('#gotry-flyai-key', goodKey);
|
|
765
|
+
await clickText('^(验证并保存|Verify and save)$', '[data-gotry-flyai-settings]');
|
|
766
|
+
await page.waitForFunction(() => /当前配置已验证|Current configuration verified/.test(document.querySelector('[data-gotry-flyai-status]')?.textContent || ''));
|
|
767
|
+
const configPath = join(homeDir, '.flyai/config.json'), beforeBad = readFileSync(configPath, 'utf8');
|
|
768
|
+
assert.equal(JSON.parse(beforeBad).FLYAI_API_KEY, goodKey);
|
|
769
|
+
assert.equal(statSync(configPath).mode & 0o777, 0o600);
|
|
770
|
+
assert.equal(await page.$eval('#gotry-flyai-key', (node) => node.value), '');
|
|
771
|
+
await page.screenshot({ path: join(outputDir, 'gotry-flyai-settings.png'), fullPage: false });
|
|
772
|
+
await page.type('#gotry-flyai-key', badKey);
|
|
773
|
+
await clickText('^(验证并保存|Verify and save)$', '[data-gotry-flyai-settings]');
|
|
774
|
+
await page.waitForFunction(() => /401/.test(document.querySelector('[data-gotry-flyai-settings] [role="alert"]')?.textContent || ''));
|
|
775
|
+
assert.equal(readFileSync(configPath, 'utf8'), beforeBad);
|
|
776
|
+
assert.equal(await page.$eval('[data-gotry-flyai-status]', (node) => /已验证|verified/.test(node.textContent || '')), true);
|
|
777
|
+
const leakedKey = await page.evaluate((keys) => keys.some(key => document.documentElement.outerHTML.includes(key)), [goodKey, badKey]);
|
|
778
|
+
assert.equal(leakedKey, false, 'a saved or rejected credential leaked into the rendered document');
|
|
779
|
+
assert.equal(serverOutput.includes(goodKey) || serverOutput.includes(badKey), false, 'a credential leaked into the runtime/browser log');
|
|
780
|
+
assert.equal(JSON.stringify(relay.bodies).includes(goodKey) || JSON.stringify(relay.bodies).includes(badKey), false, 'a credential entered model traffic');
|
|
781
|
+
await page.screenshot({ path: join(outputDir, 'gotry-flyai-rejected.png'), fullPage: false });
|
|
782
|
+
await page.setViewport({ width: 390, height: 844 });
|
|
783
|
+
await page.waitForFunction(() => (document.querySelector('[data-gotry-flyai-settings]')?.getBoundingClientRect().width || 0) >= 240);
|
|
784
|
+
const layout = await page.$eval('[data-gotry-flyai-settings]', (node) => ({ width: node.getBoundingClientRect().width, scrollWidth: node.scrollWidth }));
|
|
785
|
+
assert.ok(layout.scrollWidth <= layout.width + 1, `configuration fields overflow the narrow viewport: ${JSON.stringify(layout)}`);
|
|
786
|
+
await page.screenshot({ path: join(outputDir, 'gotry-flyai-narrow.png'), fullPage: false });
|
|
787
|
+
await page.setViewport({ width: 1440, height: 1000 });
|
|
788
|
+
await clickText('^(重新读取状态|Refresh status)$', '[data-gotry-flyai-settings]');
|
|
789
|
+
await page.waitForFunction(() => /已验证|verified/.test(document.querySelector('[data-gotry-flyai-status]')?.textContent || ''));
|
|
790
|
+
const origin = new URL(urlResult.url).origin;
|
|
791
|
+
assert.equal((await fetch(origin + '/api/gotry/flyai')).status, 401, 'configuration route bypassed native browser authentication');
|
|
792
|
+
assert.equal((await fetch(origin + '/api/gotry/flyai', { headers: { origin: 'https://untrusted.example' } })).status, 403, 'configuration route bypassed native Origin checks');
|
|
793
|
+
await clickText('^(清除本机 Key|Clear local key)$', '[data-gotry-flyai-settings]');
|
|
794
|
+
await page.waitForFunction(() => /匿名试用|Anonymous trial/.test(document.querySelector('[data-gotry-flyai-status]')?.textContent || ''));
|
|
795
|
+
assertions.flyai = { nativePluginsPage: true, maskedInput: true, verifiedSave: true, officialConfigMode: '0600',
|
|
796
|
+
rejectedCandidatePreservesOldKey: true, noDomLogModelSecret: true, refresh: true, clear: true,
|
|
797
|
+
unauthenticatedStatus: 401, crossOriginStatus: 403, narrowViewport: { width: 390, height: 844, ...layout }, verifier: 'isolated CLI fixture' };
|
|
798
|
+
assert.deepEqual(browserErrors, [], 'browser reported uncaught application errors');
|
|
799
|
+
assertions.browserErrors = browserErrors;
|
|
640
800
|
assertions.relay = { toolCalls: relay.servedTools, bodyCount: relay.bodies.length };
|
|
641
801
|
const screenshotPath = join(outputDir, 'artifact-web-e2e.png');
|
|
642
802
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
@@ -647,7 +807,7 @@ document.documentElement.setAttribute('data-${htmlSentinel}', 'host-ran-it');
|
|
|
647
807
|
const receipt = { screenshot: { path: screenshotPath, sha256: screenshotSha256 }, domAssertions: { path: domPath, sha256: domSha256 }, serverOutputSha256: createHash('sha256').update(serverOutput).digest('hex') };
|
|
648
808
|
writeFileSync(join(outputDir, 'artifact-web-e2e.receipt.json'), JSON.stringify(receipt, null, 2) + '\n', { mode: 0o600 });
|
|
649
809
|
passed = true;
|
|
650
|
-
console.log(`DSH ARTIFACT WEB E2E: PASS fresh
|
|
810
|
+
console.log(`DSH ARTIFACT WEB E2E: PASS fresh installed package; native configuration save/reject/clear, authenticated routes, persistent workbench search/page/preview, native generated-file delivery, list/read/edit/refresh; screenshot=${screenshotPath}; screenshotSha256=${screenshotSha256}; dom=${domPath}; domSha256=${domSha256}`);
|
|
651
811
|
}
|
|
652
812
|
finally {
|
|
653
813
|
if (page && !passed) {
|
|
@@ -657,6 +817,7 @@ document.documentElement.setAttribute('data-${htmlSentinel}', 'host-ran-it');
|
|
|
657
817
|
writeFileSync(failureHtml, await page.content(), { mode: 0o600 });
|
|
658
818
|
await page.screenshot({ path: failurePng, fullPage: true });
|
|
659
819
|
writeFileSync(join(outputDir, 'artifact-web-e2e.failure.log'), serverOutput, { mode: 0o600 });
|
|
820
|
+
writeFileSync(join(outputDir, 'artifact-web-e2e.failure.relay.json'), JSON.stringify({ servedTools: relay.servedTools, bodies: relay.bodies }, null, 2), { mode: 0o600 });
|
|
660
821
|
console.error(`DSH ARTIFACT WEB E2E failure artifacts: html=${failureHtml}; screenshot=${failurePng}; log=${join(outputDir, 'artifact-web-e2e.failure.log')}`);
|
|
661
822
|
}
|
|
662
823
|
catch { /* browser may have failed before a document existed */ }
|