@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
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 +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh artifact view isolated Host contract proof (issue #285).
|
|
3
|
+
*
|
|
4
|
+
* 真实执行:在临时 stateRoot/cwd 下装载 gotry-tools 插件,触发 gotry_artifacts_list
|
|
5
|
+
* + gotry_artifacts_read 的 execute + output.presentationMeta + presentResult,断言
|
|
6
|
+
* DSH 0.1.5-rc.1 的 Host ToolResult contract(SearchPathsResultView /
|
|
7
|
+
* ReadResultView / GenericCallView.locations),并跑一次隔离 select → preview →
|
|
8
|
+
* modify → view-updated 文件读取循环。
|
|
9
|
+
*
|
|
10
|
+
* 本脚本不代替 Web renderer E2E:它证明的是 DSH host 装载 → 真实 execute →
|
|
11
|
+
* 持久化 meta → Host presenter contract。fresh-profile Web custom-card 路径由
|
|
12
|
+
* 显式命令 `npx tsx scripts/dsh-artifact-web-e2e.ts` 另行覆盖;该浏览器命令
|
|
13
|
+
* 不属于跨平台 full suite。
|
|
14
|
+
*
|
|
15
|
+
* 隔离:全临时目录;不写 gotry-state;结束即删。
|
|
16
|
+
*/
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { mkdtempSync, rmSync, writeFileSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { apply } from '../src/index.js';
|
|
22
|
+
function toolResult(tool, args, value) {
|
|
23
|
+
return {
|
|
24
|
+
isError: false,
|
|
25
|
+
content: tool.output.render(args, value),
|
|
26
|
+
meta: tool.output.presentationMeta?.(args, value),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function main() {
|
|
30
|
+
// ── 1) 隔离环境 ─────────────────────────────────────────────────────────────
|
|
31
|
+
const home = mkdtempSync(join(tmpdir(), 'gotry-285-home-'));
|
|
32
|
+
const stateRoot = join(home, 'state');
|
|
33
|
+
const cwd = join(home, 'workspace');
|
|
34
|
+
mkdirSync(stateRoot, { recursive: true });
|
|
35
|
+
mkdirSync(cwd, { recursive: true });
|
|
36
|
+
// fixture:一个工作区 md,以及后续追加新内容
|
|
37
|
+
const fixturePath = join(cwd, 'trip-2027.md');
|
|
38
|
+
writeFileSync(fixturePath, [
|
|
39
|
+
'# 行程 2027',
|
|
40
|
+
'',
|
|
41
|
+
'Day 1: 北京 → 曼谷',
|
|
42
|
+
'Day 2: 曼谷 → 清迈',
|
|
43
|
+
'Day 3: 清迈 → 清莱',
|
|
44
|
+
'Day 4: 清莱 → 曼谷',
|
|
45
|
+
'Day 5: 曼谷 → 北京',
|
|
46
|
+
].join('\n'));
|
|
47
|
+
// ── 2) 装载 gotry-tools 插件(同 dsh host 装载路径:apply(ctx, config))─────
|
|
48
|
+
const registered = [];
|
|
49
|
+
const sections = [];
|
|
50
|
+
const variables = {};
|
|
51
|
+
const preExecutes = [];
|
|
52
|
+
const ctx = {
|
|
53
|
+
tools: { register: (t) => registered.push(t) },
|
|
54
|
+
systemPrompt: {
|
|
55
|
+
section: (s) => { sections.push(s); },
|
|
56
|
+
variable: (name, provider) => { variables[name] = provider; },
|
|
57
|
+
},
|
|
58
|
+
on: (event, fn) => {
|
|
59
|
+
if (event === 'tools/pre-execute')
|
|
60
|
+
preExecutes.push(fn);
|
|
61
|
+
return () => undefined;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
apply(ctx, {
|
|
65
|
+
stateRoot,
|
|
66
|
+
timeoutMs: 30_000,
|
|
67
|
+
hbcliBin: 'hbcli-not-on-path',
|
|
68
|
+
sessionAccess: 'off',
|
|
69
|
+
});
|
|
70
|
+
const exec = { agent: { session: { header: { cwd } } } };
|
|
71
|
+
const byName = (n) => {
|
|
72
|
+
const t = registered.find(t => t.name === n);
|
|
73
|
+
if (!t)
|
|
74
|
+
throw new Error(`FAIL: 插件装载后未找到 ${n},已注册: ${registered.map(t => t.name).join(',')}`);
|
|
75
|
+
return t;
|
|
76
|
+
};
|
|
77
|
+
// ── 3) select 阶段:gotry_artifacts_list 真实 execute + presentResult ─────
|
|
78
|
+
const listTool = byName('gotry_artifacts_list');
|
|
79
|
+
const listPayload = await listTool.execute({ limit: 20 }, exec);
|
|
80
|
+
assert.equal(listPayload.ok, true, 'list execute 应 ok');
|
|
81
|
+
assert.ok(Array.isArray(listPayload.artifacts) && listPayload.artifacts.length >= 1, 'list 应发现 fixture md');
|
|
82
|
+
const fixtureEntry = listPayload.artifacts.find(a => a.path?.endsWith('/trip-2027.md'));
|
|
83
|
+
assert.ok(fixtureEntry, `list 应包含 trip-2027.md,实际: ${listPayload.artifacts.map(a => a.path).join(',')}`);
|
|
84
|
+
console.log(`[1/4] SELECT list.execute → ${listPayload.artifacts.length} 项 (含 fixture trip-2027.md)`);
|
|
85
|
+
const listView = listTool.presentResult?.({}, toolResult(listTool, {}, listPayload));
|
|
86
|
+
assert.equal(listView?.card, 'search', 'list card 应为 "search" (SearchPathsResultView)');
|
|
87
|
+
assert.equal(listView?.shape, 'paths', 'list shape 应为 "paths" (SearchPathsResultView)');
|
|
88
|
+
assert.ok(Array.isArray(listView?.paths), 'list paths 应为数组(UI 渲染 deliverables 列表直接绑路径)');
|
|
89
|
+
assert.deepEqual(listView?.paths, listPayload.artifacts.map(a => a.path), 'list paths 数组应与 artifacts 顺序一致');
|
|
90
|
+
assert.equal(listView?.total, listPayload.total, 'list total 应与 execute 一致');
|
|
91
|
+
assert.equal(listView?.truncated, Boolean(listPayload.truncated), 'list truncated 应与 execute 一致');
|
|
92
|
+
const listFallback = listTool.output.render({}, listPayload);
|
|
93
|
+
assert.ok((listFallback[0]?.text ?? '').length > 0, 'list output.render 应带 summary,供无 search 卡能力的 UI fallback');
|
|
94
|
+
console.log(`[1/4] CONTRACT list → SearchPathsResultView{ card:'search', shape:'paths', paths:[${listView.paths.length}], total=${listView.total}, truncated=${listView.truncated} }`);
|
|
95
|
+
// ── 4) preview 阶段:gotry_artifacts_read 真实 execute + presentResult ───
|
|
96
|
+
const readTool = byName('gotry_artifacts_read');
|
|
97
|
+
const readCall = readTool.presentCall?.({ path: 'trip-2027.md' });
|
|
98
|
+
assert.equal(readCall?.kind, 'read', 'read presentCall kind 应为 "read"(editor follow-along)');
|
|
99
|
+
assert.ok(Array.isArray(readCall?.locations), 'read presentCall locations 应为数组');
|
|
100
|
+
assert.equal(readCall?.locations?.[0]?.path, 'trip-2027.md', 'read locations[0].path 应等于目标路径');
|
|
101
|
+
assert.equal(readCall?.locations?.[0]?.line, 1, 'read locations[0].line 应为 1(call 阶段就锁住身份)');
|
|
102
|
+
console.log(`[2/4] PREVIEW read.presentCall → GenericCallView{ kind:'read', locations:[{path:'trip-2027.md',line:1}] }`);
|
|
103
|
+
const read1Payload = await readTool.execute({ path: 'trip-2027.md' }, exec);
|
|
104
|
+
assert.equal(read1Payload.ok, true, 'read 应 ok');
|
|
105
|
+
assert.equal(read1Payload.source, 'cwd', 'read 在 cwd 中应分类为 cwd source');
|
|
106
|
+
assert.equal(read1Payload.totalLines, 7, 'fixture 7 行(标题+空行+5 day+空尾)');
|
|
107
|
+
assert.equal(read1Payload.lines?.[0]?.number, 1, '首行 number 应为 1(file line numbering 保留)');
|
|
108
|
+
assert.ok(read1Payload.lines?.[0]?.text.includes('行程 2027'), '首行应为标题');
|
|
109
|
+
assert.equal(read1Payload.lang, 'markdown', '扩展名映射 lang=markdown');
|
|
110
|
+
console.log(`[2/4] PREVIEW read.execute → { source:'${read1Payload.source}', totalLines:${read1Payload.totalLines}, lines:[${read1Payload.lines?.length}], lang:'${read1Payload.lang}' }`);
|
|
111
|
+
const read1View = readTool.presentResult?.({ path: 'trip-2027.md' }, toolResult(readTool, { path: 'trip-2027.md' }, read1Payload));
|
|
112
|
+
assert.equal(read1View?.card, 'read', 'read card 应为 "read" (ReadResultView)');
|
|
113
|
+
assert.equal(read1View?.path, read1Payload.path, 'read view path 应等于 execute path');
|
|
114
|
+
assert.equal(read1View?.offset, read1Payload.offset, 'read view offset 应保留(call→result 一致)');
|
|
115
|
+
assert.equal(read1View?.lines?.length, read1Payload.lines?.length, 'read view lines 长度应等于 execute lines');
|
|
116
|
+
assert.equal(read1View?.totalLines, read1Payload.totalLines, 'read view totalLines 应等于 execute totalLines');
|
|
117
|
+
assert.equal(read1View?.lang, 'markdown', 'read view lang 应为 markdown');
|
|
118
|
+
const identity = read1View?.content?.[0]?.text ?? '';
|
|
119
|
+
assert.ok(identity.includes('source:'), 'read fallback content[0] 应含 source 身份行');
|
|
120
|
+
assert.ok(identity.includes('cwd'), `read identity 应明示 source=cwd,实际: ${identity}`);
|
|
121
|
+
assert.ok(identity.includes(read1Payload.path ?? ''), 'read identity 应含完整 path(便于 UI 高亮与跳转)');
|
|
122
|
+
assert.ok((read1View?.content?.[1]?.text ?? '').includes('行程 2027'), 'read fallback content[1] 应为正文');
|
|
123
|
+
console.log(`[2/4] CONTRACT read → ReadResultView{ card:'read', path:'${read1View.path}', totalLines:${read1View.totalLines}, identity:'${identity.slice(0, 80)}...' }`);
|
|
124
|
+
// ── 5) modify 阶段:写入新内容到 fixture(模拟 agent/用户在侧栏修产物)──
|
|
125
|
+
appendFileSync(fixturePath, '\nDay 6: 曼谷 → 普吉\nDay 7: 普吉 → 曼谷\nDay 8: 曼谷 → 北京');
|
|
126
|
+
console.log(`[3/4] MODIFY fixture 追加 3 行,模拟产物版本变更`);
|
|
127
|
+
// ── 6) view-updated 阶段:再 read,断言看到新版本 ───────────────────────
|
|
128
|
+
const read2Payload = await readTool.execute({ path: 'trip-2027.md' }, exec);
|
|
129
|
+
assert.equal(read2Payload.ok, true, 'view-updated read 应 ok');
|
|
130
|
+
assert.equal(read2Payload.totalLines, 10, '新版本 totalLines 应为 10(原 7 + 3)');
|
|
131
|
+
assert.ok(read2Payload.lines?.[0]?.text.includes('行程 2027'), 'view-updated 首行仍为标题');
|
|
132
|
+
assert.ok(read2Payload.lines?.some(l => l.text.includes('Day 6')), 'view-updated 应包含新追加 Day 6');
|
|
133
|
+
const read2View = readTool.presentResult?.({ path: 'trip-2027.md' }, toolResult(readTool, { path: 'trip-2027.md' }, read2Payload));
|
|
134
|
+
assert.equal(read2View?.totalLines, 10, 'view-updated ReadResultView totalLines 应等于 10');
|
|
135
|
+
assert.ok((read2View?.content?.[1]?.text ?? '').includes('Day 6'), 'view-updated fallback content 应含 Day 6(UI 重新打开产物拿到最新版本)');
|
|
136
|
+
console.log(`[3/4] VIEW-UPDATED read.execute → { totalLines:${read2Payload.totalLines}, lines:[${read2Payload.lines?.length}] },ReadResultView.content 含 Day 6`);
|
|
137
|
+
// ── 7) 边界:越界路径 / 非白名单扩展名 / 不存在文件 ────────────────────
|
|
138
|
+
const outOfScope = await readTool.execute({ path: '../../../../etc/passwd' }, exec);
|
|
139
|
+
assert.equal(outOfScope.ok, false, '越界路径必须被拒');
|
|
140
|
+
assert.ok(/scope|outside|workspace|越界/i.test(outOfScope.error ?? ''), `越界错误信息应明示 scope,实际: ${outOfScope.error}`);
|
|
141
|
+
console.log(`[4/4] GUARDRAIL 越界路径拒 ok=false,error 含 scope 类字样`);
|
|
142
|
+
const notFound = await readTool.execute({ path: 'never-written.md' }, exec);
|
|
143
|
+
assert.equal(notFound.ok, false, '不存在文件应 ok=false(不静默 ok)');
|
|
144
|
+
assert.ok(/not found|不存在/i.test(notFound.error ?? ''), `不存在错误信息应明示 not found,实际: ${notFound.error}`);
|
|
145
|
+
console.log(`[4/4] GUARDRAIL 不存在文件拒 ok=false,error 含 not-found 类字样`);
|
|
146
|
+
// ── 清理 ────────────────────────────────────────────────────────────────
|
|
147
|
+
rmSync(home, { recursive: true, force: true });
|
|
148
|
+
console.log('\nDSH ARTIFACT HOST CONTRACT PROOF: list/read metadata, source identity, view-updated loop, guardrails OK; fresh-profile Web custom-card path covered separately by `npx tsx scripts/dsh-artifact-web-e2e.ts` (not part of the cross-platform full suite)');
|
|
149
|
+
}
|
|
150
|
+
await main();
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fresh-profile, installed-package Web E2E for GoTry artifact cards.
|
|
3
|
+
*
|
|
4
|
+
* The only model endpoint is a local OpenAI-compatible SSE relay. The test
|
|
5
|
+
* starts `gotry web --no-open --port 0`, captures the authenticated `dsh web:`
|
|
6
|
+
* URL, then drives the real composer with a fresh headless Chrome profile.
|
|
7
|
+
* It verifies the public Client cards, path selection, the normal read → edit
|
|
8
|
+
* → gotry_artifacts_read sequence, and the visibly refreshed content/version.
|
|
9
|
+
* Temporary package install, HOME/DSH_HOME, workspace, relay, browser, and
|
|
10
|
+
* server state are cleaned on both success and failure.
|
|
11
|
+
*/
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { createServer } from 'node:http';
|
|
15
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { basename, join } from 'node:path';
|
|
18
|
+
import { spawn } from 'node:child_process';
|
|
19
|
+
import { promisify } from 'node:util';
|
|
20
|
+
import { execFile as execFileCallback } from 'node:child_process';
|
|
21
|
+
import puppeteer from 'puppeteer-core';
|
|
22
|
+
const execFile = promisify(execFileCallback);
|
|
23
|
+
const ROOT = join(import.meta.dirname, '..', '..');
|
|
24
|
+
const CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
25
|
+
const TIMEOUT_MS = 120_000;
|
|
26
|
+
function sse(data) {
|
|
27
|
+
return `data: ${JSON.stringify(data)}\n\n`;
|
|
28
|
+
}
|
|
29
|
+
function toolResponse(id, name, args) {
|
|
30
|
+
return sse({
|
|
31
|
+
id,
|
|
32
|
+
object: 'chat.completion.chunk',
|
|
33
|
+
created: 1,
|
|
34
|
+
model: 'synthetic-artifact-web',
|
|
35
|
+
choices: [{ index: 0, delta: { role: 'assistant', tool_calls: [{ index: 0, id: `${id}-call`, type: 'function', function: { name, arguments: JSON.stringify(args) } }] }, finish_reason: null }],
|
|
36
|
+
}) + sse({
|
|
37
|
+
id: `${id}-finish`,
|
|
38
|
+
object: 'chat.completion.chunk',
|
|
39
|
+
created: 1,
|
|
40
|
+
model: 'synthetic-artifact-web',
|
|
41
|
+
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
|
|
42
|
+
}) + 'data: [DONE]\n\n';
|
|
43
|
+
}
|
|
44
|
+
function textResponse(id, text) {
|
|
45
|
+
return sse({
|
|
46
|
+
id,
|
|
47
|
+
object: 'chat.completion.chunk',
|
|
48
|
+
created: 1,
|
|
49
|
+
model: 'synthetic-artifact-web',
|
|
50
|
+
choices: [{ index: 0, delta: { role: 'assistant', content: text }, finish_reason: null }],
|
|
51
|
+
}) + sse({
|
|
52
|
+
id: `${id}-finish`,
|
|
53
|
+
object: 'chat.completion.chunk',
|
|
54
|
+
created: 1,
|
|
55
|
+
model: 'synthetic-artifact-web',
|
|
56
|
+
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
|
57
|
+
}) + 'data: [DONE]\n\n';
|
|
58
|
+
}
|
|
59
|
+
function toolNames(body) {
|
|
60
|
+
return (body.tools ?? []).map(tool => {
|
|
61
|
+
const fn = tool.function;
|
|
62
|
+
return String(fn?.name ?? tool.name ?? '');
|
|
63
|
+
}).filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
function hasToolMessage(body) {
|
|
66
|
+
return (body.messages ?? []).some(message => message.role === 'tool');
|
|
67
|
+
}
|
|
68
|
+
function lastUserText(body) {
|
|
69
|
+
const message = [...(body.messages ?? [])].reverse().find(item => item.role === 'user');
|
|
70
|
+
const content = message?.content;
|
|
71
|
+
if (typeof content === 'string')
|
|
72
|
+
return content;
|
|
73
|
+
return JSON.stringify(content ?? '');
|
|
74
|
+
}
|
|
75
|
+
async function startRelay() {
|
|
76
|
+
const bodies = [];
|
|
77
|
+
const servedTools = [];
|
|
78
|
+
let phase = 'first-list';
|
|
79
|
+
const server = createServer((req, res) => {
|
|
80
|
+
if (req.method !== 'POST' || !req.url?.endsWith('/chat/completions')) {
|
|
81
|
+
res.writeHead(404).end('not found');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const chunks = [];
|
|
85
|
+
req.on('data', chunk => chunks.push(Buffer.from(chunk)));
|
|
86
|
+
req.on('end', () => {
|
|
87
|
+
let body = {};
|
|
88
|
+
try {
|
|
89
|
+
body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
90
|
+
}
|
|
91
|
+
catch { /* dsh will report malformed upstream */ }
|
|
92
|
+
bodies.push(body);
|
|
93
|
+
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
|
|
94
|
+
const names = toolNames(body);
|
|
95
|
+
const hasArtifacts = names.includes('gotry_artifacts_list') || names.includes('gotry_artifacts_read');
|
|
96
|
+
let payload;
|
|
97
|
+
if (!hasArtifacts) {
|
|
98
|
+
payload = textResponse(`aux-${bodies.length}`, '');
|
|
99
|
+
}
|
|
100
|
+
else if (phase === 'first-list') {
|
|
101
|
+
phase = 'first-read';
|
|
102
|
+
servedTools.push('gotry_artifacts_list');
|
|
103
|
+
payload = toolResponse('artifact-first-list', 'gotry_artifacts_list', { limit: 20 });
|
|
104
|
+
}
|
|
105
|
+
else if (phase === 'first-read' && hasToolMessage(body)) {
|
|
106
|
+
phase = 'first-final';
|
|
107
|
+
servedTools.push('gotry_artifacts_read');
|
|
108
|
+
payload = toolResponse('artifact-first-read', 'gotry_artifacts_read', { path: 'trip-2027.md' });
|
|
109
|
+
}
|
|
110
|
+
else if (phase === 'first-final' && hasToolMessage(body)) {
|
|
111
|
+
phase = 'second-read';
|
|
112
|
+
payload = textResponse('artifact-first-final', '已展示行程产物。');
|
|
113
|
+
}
|
|
114
|
+
else if (phase === 'second-read' && lastUserText(body).includes('修改')) {
|
|
115
|
+
phase = 'second-edit';
|
|
116
|
+
servedTools.push('read');
|
|
117
|
+
payload = toolResponse('artifact-second-read', 'read', { file_path: 'trip-2027.md' });
|
|
118
|
+
}
|
|
119
|
+
else if (phase === 'second-edit' && hasToolMessage(body)) {
|
|
120
|
+
phase = 'second-artifact-read';
|
|
121
|
+
servedTools.push('edit');
|
|
122
|
+
payload = toolResponse('artifact-second-edit', 'edit', { file_path: 'trip-2027.md', old_string: 'Day 5: return', new_string: 'Day 5: return\nDay 6: revision requested', replace_all: false });
|
|
123
|
+
}
|
|
124
|
+
else if (phase === 'second-artifact-read' && hasToolMessage(body)) {
|
|
125
|
+
phase = 'second-final';
|
|
126
|
+
servedTools.push('gotry_artifacts_read');
|
|
127
|
+
payload = toolResponse('artifact-second-artifact-read', 'gotry_artifacts_read', { path: 'trip-2027.md' });
|
|
128
|
+
}
|
|
129
|
+
else if (phase === 'second-final' && hasToolMessage(body)) {
|
|
130
|
+
payload = textResponse('artifact-second-final', '已更新并重新读取行程产物。');
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
payload = textResponse(`unexpected-${bodies.length}`, '');
|
|
134
|
+
}
|
|
135
|
+
res.end(payload);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
139
|
+
return {
|
|
140
|
+
port: server.address().port,
|
|
141
|
+
bodies,
|
|
142
|
+
servedTools,
|
|
143
|
+
close: () => new Promise(resolve => server.close(() => resolve())),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function run(command, args, options) {
|
|
147
|
+
const result = await execFile(command, args, { cwd: options.cwd, env: options.env, maxBuffer: 16 * 1024 * 1024 });
|
|
148
|
+
return result.stdout;
|
|
149
|
+
}
|
|
150
|
+
function waitForLine(child, pattern, timeoutMs) {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
let output = '';
|
|
153
|
+
const timer = setTimeout(() => { reject(new Error(`timed out waiting for ${pattern}; output=${output.slice(-4000)}`)); }, timeoutMs);
|
|
154
|
+
const onData = (chunk) => {
|
|
155
|
+
output += chunk.toString();
|
|
156
|
+
const match = output.match(pattern);
|
|
157
|
+
if (!match)
|
|
158
|
+
return;
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
child.stdout.off('data', onData);
|
|
161
|
+
child.stderr.off('data', onData);
|
|
162
|
+
resolve({ url: match[1], output });
|
|
163
|
+
};
|
|
164
|
+
child.stdout.on('data', onData);
|
|
165
|
+
child.stderr.on('data', onData);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async function main() {
|
|
169
|
+
assert.ok(existsSync(CHROME), `Chrome missing: ${CHROME}`);
|
|
170
|
+
const outputDir = process.env.GOTRY_ARTIFACT_WEB_E2E_OUT || mkdtempSync(join(tmpdir(), 'gotry-artifact-web-evidence-'));
|
|
171
|
+
mkdirSync(outputDir, { recursive: true });
|
|
172
|
+
const packageDir = mkdtempSync(join(tmpdir(), 'gotry-artifact-web-pack-'));
|
|
173
|
+
const consumerDir = mkdtempSync(join(tmpdir(), 'gotry-artifact-web-consumer-'));
|
|
174
|
+
const workspaceDir = mkdtempSync(join(tmpdir(), 'gotry-artifact-web-workspace-'));
|
|
175
|
+
const homeDir = mkdtempSync(join(tmpdir(), 'gotry-artifact-web-home-'));
|
|
176
|
+
const dshHome = mkdtempSync(join(tmpdir(), 'gotry-artifact-web-dsh-'));
|
|
177
|
+
const userDataDir = mkdtempSync(join(tmpdir(), 'gotry-artifact-web-chrome-'));
|
|
178
|
+
const fixture = '# Trip 2027\n\nDay 1: arrival\nDay 2: city walk\nDay 3: museum\nDay 4: rest\nDay 5: return\n';
|
|
179
|
+
writeFileSync(join(workspaceDir, 'trip-2027.md'), fixture, { mode: 0o600 });
|
|
180
|
+
writeFileSync(join(consumerDir, 'package.json'), JSON.stringify({ private: true, dependencies: {} }, null, 2));
|
|
181
|
+
const relay = await startRelay();
|
|
182
|
+
let browser = null;
|
|
183
|
+
let page = null;
|
|
184
|
+
let server = null;
|
|
185
|
+
let passed = false;
|
|
186
|
+
let serverOutput = '';
|
|
187
|
+
const assertions = {
|
|
188
|
+
isolation: { home: homeDir, dshHome, workspace: workspaceDir, userDataDir, liveFlags: { GOTRY_SESSION_LIVE: '0', GOTRY_HBCLI_LIVE: '0', GOTRY_HOTELBYTE_SKILLS_LIVE: '0' } },
|
|
189
|
+
};
|
|
190
|
+
try {
|
|
191
|
+
await run(process.execPath, [join(ROOT, 'scripts/build-dist.mjs')], { cwd: ROOT, env: process.env });
|
|
192
|
+
await run('npm', ['pack', '--ignore-scripts', '--pack-destination', packageDir], { cwd: ROOT, env: process.env });
|
|
193
|
+
const tarball = join(packageDir, readdirSync(packageDir).find(name => name.endsWith('.tgz')) || '');
|
|
194
|
+
assert.ok(existsSync(tarball), 'npm pack did not produce a GoTry tarball');
|
|
195
|
+
const packageJson = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
|
|
196
|
+
await run('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], { cwd: consumerDir, env: { ...process.env, HOME: homeDir } });
|
|
197
|
+
const installedRoot = join(consumerDir, 'node_modules', '@danceiny', 'gotry');
|
|
198
|
+
const installedBin = join(installedRoot, 'bin', 'gotry-inner.js');
|
|
199
|
+
assert.ok(existsSync(installedBin), 'packed GoTry binary missing');
|
|
200
|
+
assert.ok(existsSync(join(installedRoot, 'client', 'client.js')), 'packed public client missing');
|
|
201
|
+
assert.equal(JSON.parse(readFileSync(join(installedRoot, 'package.json'), 'utf8')).exports['./client'], './client/client.js');
|
|
202
|
+
assertions.package = { version: packageJson.version, tarball: basename(tarball), installedClient: join(installedRoot, 'client', 'client.js') };
|
|
203
|
+
const env = {
|
|
204
|
+
...process.env,
|
|
205
|
+
HOME: homeDir,
|
|
206
|
+
DSH_HOME: dshHome,
|
|
207
|
+
GOTRY_ONBOARDING_SKIP: '1',
|
|
208
|
+
GOTRY_SETUP_SKIP: '1',
|
|
209
|
+
GOTRY_SESSION_LIVE: '0',
|
|
210
|
+
GOTRY_HBCLI_LIVE: '0',
|
|
211
|
+
GOTRY_HOTELBYTE_SKILLS_LIVE: '0',
|
|
212
|
+
LLM_API_KEY: 'synthetic-artifact-web-key',
|
|
213
|
+
LLM_BASE_URL: `http://127.0.0.1:${relay.port}/v1`,
|
|
214
|
+
LLM_MODEL: 'synthetic-artifact-web',
|
|
215
|
+
};
|
|
216
|
+
const child = spawn(process.execPath, [installedBin, 'web', '--no-open', '--port', '0'], { cwd: workspaceDir, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
217
|
+
server = child;
|
|
218
|
+
child.stdout.on('data', chunk => { serverOutput += chunk.toString(); });
|
|
219
|
+
child.stderr.on('data', chunk => { serverOutput += chunk.toString(); });
|
|
220
|
+
const urlResult = await waitForLine(child, /dsh web: (http:\/\/127\.0\.0\.1:\d+\/\?token=[^\s)]+)/, TIMEOUT_MS);
|
|
221
|
+
serverOutput += urlResult.output;
|
|
222
|
+
assertions.server = { authenticatedUrlCaptured: true, urlHost: new URL(urlResult.url).host, port: new URL(urlResult.url).port };
|
|
223
|
+
browser = await puppeteer.launch({ executablePath: CHROME, headless: true, userDataDir, args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'] });
|
|
224
|
+
page = await browser.newPage();
|
|
225
|
+
page.on('console', (message) => { serverOutput += `\n[browser console ${message.type()}] ${message.text()}`; });
|
|
226
|
+
page.on('pageerror', (error) => { serverOutput += `\n[browser pageerror] ${error.stack || error.message}`; });
|
|
227
|
+
page.setDefaultTimeout(30_000);
|
|
228
|
+
await page.goto(urlResult.url, { waitUntil: 'networkidle2', timeout: 30_000 });
|
|
229
|
+
// A fresh DSH profile may show the product's first-run disclosure before
|
|
230
|
+
// the composer is mounted. Dismiss that visible UI gate through its real
|
|
231
|
+
// button; do not seed cookies or attach an existing browser profile.
|
|
232
|
+
await page.evaluate(() => {
|
|
233
|
+
const button = [...document.querySelectorAll('button')].find(node => node.textContent?.trim() === '继续');
|
|
234
|
+
button?.click();
|
|
235
|
+
});
|
|
236
|
+
// Register the isolated fixture through DSH's authenticated public RPC
|
|
237
|
+
// carrier. The browser still selects the resulting workspace through the
|
|
238
|
+
// real workspace picker; no DSH registry file or user profile is seeded.
|
|
239
|
+
const workspaceTitle = basename(workspaceDir);
|
|
240
|
+
const workspaceRpc = await page.evaluate(async (path) => {
|
|
241
|
+
const rpcId = crypto.randomUUID();
|
|
242
|
+
const response = await fetch(`/api/workspace/create`, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: { 'content-type': 'application/json' },
|
|
245
|
+
body: JSON.stringify({
|
|
246
|
+
type: 'client-request',
|
|
247
|
+
rpcId,
|
|
248
|
+
method: 'workspace/create',
|
|
249
|
+
payload: { args: { request: { path } } },
|
|
250
|
+
}),
|
|
251
|
+
});
|
|
252
|
+
return { status: response.status, body: await response.text() };
|
|
253
|
+
}, workspaceDir);
|
|
254
|
+
assert.equal(workspaceRpc.status, 200, `workspace RPC failed: ${workspaceRpc.status} ${workspaceRpc.body}`);
|
|
255
|
+
const workspaceResult = JSON.parse(workspaceRpc.body);
|
|
256
|
+
assert.equal(workspaceResult.result?.ok, true, `workspace RPC rejected: ${workspaceRpc.body}`);
|
|
257
|
+
assertions.workspace = { rpc: 'workspace/create', title: workspaceResult.result?.value?.workspace?.title || workspaceTitle };
|
|
258
|
+
// Reconnect the public workspace feed so the browser observes the
|
|
259
|
+
// host-authoritative registration through its normal baseline path.
|
|
260
|
+
await page.reload({ waitUntil: 'networkidle2', timeout: 30_000 });
|
|
261
|
+
await page.evaluate(() => {
|
|
262
|
+
const button = [...document.querySelectorAll('button')].find(node => node.textContent?.trim() === '继续');
|
|
263
|
+
button?.click();
|
|
264
|
+
});
|
|
265
|
+
await page.waitForFunction((title) => document.body.innerText.includes(title), { timeout: 30_000 }, workspaceTitle);
|
|
266
|
+
await page.click('button[aria-label="选择工作区"], button[aria-label="Select workspace"]');
|
|
267
|
+
await page.waitForFunction((title) => [...document.querySelectorAll('[role="menuitem"], [role="option"], button')].some(node => node.textContent?.trim() === title), { timeout: 30_000 }, workspaceTitle);
|
|
268
|
+
await page.evaluate((title) => {
|
|
269
|
+
const node = [...document.querySelectorAll('[role="menuitem"], [role="option"], button')].find(candidate => candidate.textContent?.trim() === title);
|
|
270
|
+
node?.click();
|
|
271
|
+
}, workspaceTitle);
|
|
272
|
+
await page.waitForSelector('[contenteditable="true"]');
|
|
273
|
+
const submit = async (text) => {
|
|
274
|
+
const editor = await page.$('[contenteditable="true"]');
|
|
275
|
+
assert.ok(editor, 'real composer missing');
|
|
276
|
+
await editor.click();
|
|
277
|
+
await page.keyboard.type(text);
|
|
278
|
+
const button = await page.$('button[aria-label="发送消息"], button[aria-label="Send message"]');
|
|
279
|
+
assert.ok(button, 'real composer submit button missing');
|
|
280
|
+
await button.click();
|
|
281
|
+
};
|
|
282
|
+
await submit('请列出并打开刚才生成的 trip-2027.md 行程产物。');
|
|
283
|
+
const revealArtifactCalls = async () => {
|
|
284
|
+
await page.evaluate(() => {
|
|
285
|
+
for (const process of document.querySelectorAll('[data-turn-process]')) {
|
|
286
|
+
if (process.getAttribute('aria-expanded') !== 'true')
|
|
287
|
+
process.click();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
await page.waitForFunction(() => document.querySelector('[data-turn-process="1"]') !== null, { timeout: 60_000 });
|
|
292
|
+
await revealArtifactCalls();
|
|
293
|
+
await page.waitForSelector('[data-gotry-artifact-card="list"]');
|
|
294
|
+
await page.waitForSelector('[data-gotry-artifact-card="list"][data-gotry-artifact-state="ok"]');
|
|
295
|
+
await page.waitForSelector('[data-gotry-artifact-card="read"] [data-gotry-artifact-line="1"]');
|
|
296
|
+
assert.equal(await page.$eval('[data-gotry-artifact-card="list"]', (node) => {
|
|
297
|
+
const rect = node.getBoundingClientRect();
|
|
298
|
+
return rect.width > 0 && rect.height > 0;
|
|
299
|
+
}), true, 'artifact list card is not visibly rendered');
|
|
300
|
+
assert.equal(await page.$eval('[data-gotry-artifact-card="read"]', (node) => {
|
|
301
|
+
const rect = node.getBoundingClientRect();
|
|
302
|
+
return rect.width > 0 && rect.height > 0;
|
|
303
|
+
}), true, 'artifact read card is not visibly rendered');
|
|
304
|
+
const pathButton = await page.$('[data-gotry-artifact-path$="trip-2027.md"]');
|
|
305
|
+
assert.ok(pathButton, 'list card path button missing');
|
|
306
|
+
await pathButton.click();
|
|
307
|
+
await page.waitForSelector('[data-gotry-artifact-selection$="trip-2027.md"]');
|
|
308
|
+
const firstSnapshot = await page.evaluate(() => ({
|
|
309
|
+
listCards: document.querySelectorAll('[data-gotry-artifact-card="list"]').length,
|
|
310
|
+
readCards: document.querySelectorAll('[data-gotry-artifact-card="read"]').length,
|
|
311
|
+
paths: [...document.querySelectorAll('[data-gotry-artifact-path]')].map(node => node.textContent),
|
|
312
|
+
selected: document.querySelector('[data-gotry-artifact-selection]')?.textContent || null,
|
|
313
|
+
identity: document.querySelector('[data-gotry-artifact-identity]')?.textContent || null,
|
|
314
|
+
line1: document.querySelector('[data-gotry-artifact-line="1"]')?.textContent || null,
|
|
315
|
+
}));
|
|
316
|
+
assert.equal(firstSnapshot.listCards, 1);
|
|
317
|
+
assert.ok(firstSnapshot.readCards >= 1);
|
|
318
|
+
assert.ok(firstSnapshot.paths.some((path) => path?.endsWith('trip-2027.md')));
|
|
319
|
+
assert.ok(firstSnapshot.selected?.endsWith('trip-2027.md'));
|
|
320
|
+
assert.match(firstSnapshot.identity || '', /来源|source/);
|
|
321
|
+
assertions.firstTurn = firstSnapshot;
|
|
322
|
+
await submit('请修改刚才的行程产物,在末尾追加 Day 6,然后重新读取并展示更新版本。');
|
|
323
|
+
await page.waitForFunction(() => document.querySelector('[data-turn-process="2"]') !== null, { timeout: 60_000 });
|
|
324
|
+
await revealArtifactCalls();
|
|
325
|
+
await page.waitForFunction(() => document.querySelectorAll('[data-gotry-artifact-card="read"]').length >= 2, { timeout: 60_000 });
|
|
326
|
+
await page.waitForFunction(() => [...document.querySelectorAll('[data-gotry-artifact-card="read"]')].some(node => node.textContent?.includes('Day 6: revision requested')), { timeout: 60_000 });
|
|
327
|
+
// The artifact card has the fresh read result, but the public DSH file
|
|
328
|
+
// preview intentionally keeps its old page until the user acknowledges
|
|
329
|
+
// the changed-file banner. Exercise that visible reload control instead
|
|
330
|
+
// of treating the custom card or hidden DOM as sidebar evidence.
|
|
331
|
+
await page.waitForSelector('[data-textpreview-changed]', { timeout: 60_000 });
|
|
332
|
+
const staleRightPreview = await page.$eval('[data-textpreview-state="text"]', (node) => {
|
|
333
|
+
const rect = node.getBoundingClientRect();
|
|
334
|
+
const body = node.querySelector('[data-textpreview-body]');
|
|
335
|
+
return {
|
|
336
|
+
visible: rect.width > 0 && rect.height > 0,
|
|
337
|
+
changedBanner: Boolean(node.querySelector('[data-textpreview-changed]')),
|
|
338
|
+
includesDay6: body?.textContent?.includes('Day 6: revision requested') || false,
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
assert.equal(staleRightPreview.visible, true, 'right file preview is not visibly rendered before reload');
|
|
342
|
+
assert.equal(staleRightPreview.changedBanner, true, 'right file preview did not show the changed-file banner');
|
|
343
|
+
assert.equal(staleRightPreview.includesDay6, false, 'right file preview unexpectedly contained Day 6 before reload');
|
|
344
|
+
const reloadButton = await page.$('[data-textpreview-changed] [data-textpreview-reload-now]');
|
|
345
|
+
assert.ok(reloadButton, 'right file preview reload control missing');
|
|
346
|
+
const reloadControl = await page.$eval('[data-textpreview-changed] [data-textpreview-reload-now]', (node) => {
|
|
347
|
+
const rect = node.getBoundingClientRect();
|
|
348
|
+
return {
|
|
349
|
+
visible: rect.width > 0 && rect.height > 0,
|
|
350
|
+
text: node.textContent?.trim() || '',
|
|
351
|
+
ariaLabel: node.getAttribute('aria-label'),
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
assert.equal(reloadControl.visible, true, 'right file preview reload control is not visible');
|
|
355
|
+
assert.match(reloadControl.text, /重新载入|Reload/);
|
|
356
|
+
await reloadButton.click();
|
|
357
|
+
await page.waitForFunction(() => {
|
|
358
|
+
const preview = document.querySelector('[data-textpreview-state="text"]');
|
|
359
|
+
const body = preview?.querySelector('[data-textpreview-body]');
|
|
360
|
+
return Boolean(preview && body?.textContent?.includes('Day 6: revision requested'));
|
|
361
|
+
}, { timeout: 60_000 });
|
|
362
|
+
await page.$eval('[data-textpreview-body]', (node) => {
|
|
363
|
+
const body = node;
|
|
364
|
+
body.scrollTop = body.scrollHeight;
|
|
365
|
+
});
|
|
366
|
+
const rightPreviewAfterReload = await page.$eval('[data-textpreview-state="text"]', (node) => {
|
|
367
|
+
const rect = node.getBoundingClientRect();
|
|
368
|
+
const body = node.querySelector('[data-textpreview-body]');
|
|
369
|
+
const day6 = [...(node.querySelectorAll('[data-textpreview-line]'))].find(line => line.textContent?.includes('Day 6: revision requested'));
|
|
370
|
+
const day6Rect = day6?.getBoundingClientRect();
|
|
371
|
+
return {
|
|
372
|
+
visible: rect.width > 0 && rect.height > 0,
|
|
373
|
+
changedBanner: Boolean(node.querySelector('[data-textpreview-changed]')),
|
|
374
|
+
bodyIncludesDay6: body?.textContent?.includes('Day 6: revision requested') || false,
|
|
375
|
+
day6Visible: Boolean(day6Rect && day6Rect.width > 0 && day6Rect.height > 0 && day6Rect.top < window.innerHeight && day6Rect.bottom > 0),
|
|
376
|
+
bodyText: body?.textContent || '',
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
assert.equal(rightPreviewAfterReload.visible, true, 'right file preview is not visibly rendered after reload');
|
|
380
|
+
assert.equal(rightPreviewAfterReload.changedBanner, false, 'right file preview still shows the changed-file banner after reload');
|
|
381
|
+
assert.equal(rightPreviewAfterReload.bodyIncludesDay6, true, 'right file preview body does not contain Day 6 after reload');
|
|
382
|
+
assert.equal(rightPreviewAfterReload.day6Visible, true, 'right file preview Day 6 is not visible after reload');
|
|
383
|
+
// DSH may collapse a completed turn after the last tool result arrives;
|
|
384
|
+
// reopen the real process disclosure and place the revised card in view so
|
|
385
|
+
// the receipt proves visible, not merely hidden-DOM, refreshed evidence.
|
|
386
|
+
await revealArtifactCalls();
|
|
387
|
+
await page.evaluate(() => {
|
|
388
|
+
const cards = document.querySelectorAll('[data-gotry-artifact-card="read"]');
|
|
389
|
+
cards[cards.length - 1]?.scrollIntoView({ block: 'center' });
|
|
390
|
+
});
|
|
391
|
+
assert.equal(await page.$eval('[data-gotry-artifact-card="read"]:last-of-type', (node) => {
|
|
392
|
+
const rect = node.getBoundingClientRect();
|
|
393
|
+
return rect.width > 0 && rect.height > 0;
|
|
394
|
+
}), true, 'updated artifact read card is not visibly rendered');
|
|
395
|
+
const finalSnapshot = await page.evaluate(() => ({
|
|
396
|
+
readCards: document.querySelectorAll('[data-gotry-artifact-card="read"]').length,
|
|
397
|
+
updated: [...document.querySelectorAll('[data-gotry-artifact-card="read"]')].some(node => node.textContent?.includes('Day 6: revision requested')),
|
|
398
|
+
versions: [...document.querySelectorAll('[data-gotry-artifact-version]')].map(node => node.getAttribute('data-gotry-artifact-version')),
|
|
399
|
+
identities: [...document.querySelectorAll('[data-gotry-artifact-identity]')].map(node => node.textContent),
|
|
400
|
+
updatedLine: [...document.querySelectorAll('[data-gotry-artifact-line]')].find(node => node.textContent?.includes('Day 6'))?.textContent || null,
|
|
401
|
+
}));
|
|
402
|
+
assert.ok(finalSnapshot.readCards >= 2);
|
|
403
|
+
assert.equal(finalSnapshot.updated, true);
|
|
404
|
+
assert.ok(finalSnapshot.versions.filter(Boolean).length >= 2);
|
|
405
|
+
assert.ok(finalSnapshot.updatedLine?.includes('Day 6'));
|
|
406
|
+
assertions.secondTurn = { ...finalSnapshot, rightPreview: rightPreviewAfterReload, reloadControl };
|
|
407
|
+
assert.deepEqual(relay.servedTools, ['gotry_artifacts_list', 'gotry_artifacts_read', 'read', 'edit', 'gotry_artifacts_read']);
|
|
408
|
+
assertions.relay = { toolCalls: relay.servedTools, bodyCount: relay.bodies.length };
|
|
409
|
+
const screenshotPath = join(outputDir, 'artifact-web-e2e.png');
|
|
410
|
+
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
411
|
+
const domPath = join(outputDir, 'artifact-web-e2e.dom.json');
|
|
412
|
+
writeFileSync(domPath, JSON.stringify(assertions, null, 2) + '\n', { mode: 0o600 });
|
|
413
|
+
const screenshotSha256 = createHash('sha256').update(readFileSync(screenshotPath)).digest('hex');
|
|
414
|
+
const domSha256 = createHash('sha256').update(readFileSync(domPath)).digest('hex');
|
|
415
|
+
const receipt = { screenshot: { path: screenshotPath, sha256: screenshotSha256 }, domAssertions: { path: domPath, sha256: domSha256 }, serverOutputSha256: createHash('sha256').update(serverOutput).digest('hex') };
|
|
416
|
+
writeFileSync(join(outputDir, 'artifact-web-e2e.receipt.json'), JSON.stringify(receipt, null, 2) + '\n', { mode: 0o600 });
|
|
417
|
+
passed = true;
|
|
418
|
+
console.log(`DSH ARTIFACT WEB E2E: PASS fresh Chrome profile, list→select/open→preview→read→edit→updated read; screenshot=${screenshotPath}; screenshotSha256=${screenshotSha256}; dom=${domPath}; domSha256=${domSha256}`);
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
if (page && !passed) {
|
|
422
|
+
try {
|
|
423
|
+
const failureHtml = join(outputDir, 'artifact-web-e2e.failure.html');
|
|
424
|
+
const failurePng = join(outputDir, 'artifact-web-e2e.failure.png');
|
|
425
|
+
writeFileSync(failureHtml, await page.content(), { mode: 0o600 });
|
|
426
|
+
await page.screenshot({ path: failurePng, fullPage: true });
|
|
427
|
+
writeFileSync(join(outputDir, 'artifact-web-e2e.failure.log'), serverOutput, { mode: 0o600 });
|
|
428
|
+
console.error(`DSH ARTIFACT WEB E2E failure artifacts: html=${failureHtml}; screenshot=${failurePng}; log=${join(outputDir, 'artifact-web-e2e.failure.log')}`);
|
|
429
|
+
}
|
|
430
|
+
catch { /* browser may have failed before a document existed */ }
|
|
431
|
+
}
|
|
432
|
+
await browser?.close().catch(() => { });
|
|
433
|
+
if (server && !server.killed) {
|
|
434
|
+
server.kill('SIGTERM');
|
|
435
|
+
await new Promise(resolve => { server?.once('close', () => resolve()); setTimeout(resolve, 2_000); });
|
|
436
|
+
}
|
|
437
|
+
await relay.close().catch(() => { });
|
|
438
|
+
for (const directory of [packageDir, consumerDir, workspaceDir, homeDir, dshHome, userDataDir]) {
|
|
439
|
+
rmSync(directory, { recursive: true, force: true });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
await main();
|