@danceiny/gotry 0.0.1-rc.8 → 0.2.0-rc.27
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 +172 -217
- package/README.zh-CN.md +222 -0
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +1744 -0
- package/bin/gotry-inner.js +733 -83
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +49 -0
- package/bin/gotry.js +1 -1
- package/client/client.js +203 -0
- package/cordis.gotry-patch.yml +80 -49
- package/data/airline-airports.json +39 -0
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +101 -146
- package/dist/capabilities/anything.js +135 -94
- package/dist/capabilities/artifacts.js +370 -0
- package/dist/capabilities/channel-health.js +131 -0
- package/dist/capabilities/channel-registry.js +256 -0
- package/dist/capabilities/doctor.js +546 -0
- package/dist/capabilities/effect.js +394 -0
- package/dist/capabilities/external-event.js +149 -0
- package/dist/capabilities/fact-log.js +55 -0
- package/dist/capabilities/flyai-config.js +337 -0
- package/dist/capabilities/flyai.js +968 -0
- package/dist/capabilities/fx-contract.js +293 -0
- package/dist/capabilities/geo-atlas.js +246 -0
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +260 -123
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/itinerary-artifact.js +190 -0
- package/dist/capabilities/itinerary-deck-artifact.js +194 -0
- package/dist/capabilities/itinerary-deck-export.js +339 -0
- package/dist/capabilities/lavish-local.js +924 -0
- package/dist/capabilities/model-override.js +38 -0
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +191 -0
- package/dist/capabilities/session/action-cache.js +126 -0
- package/dist/capabilities/session/adapters/ctrip-flight.js +141 -0
- package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
- package/dist/capabilities/session/adapters/dida-portal.js +274 -0
- package/dist/capabilities/session/adapters/meituan-local.js +54 -0
- package/dist/capabilities/session/adapters/rail-12306.js +342 -0
- package/dist/capabilities/session/benchmark.js +189 -0
- package/dist/capabilities/session/bridge-ledger.js +224 -0
- package/dist/capabilities/session/extension-bridge.js +636 -0
- package/dist/capabilities/session/extension-channel.js +109 -0
- package/dist/capabilities/session/extension-distribution.js +230 -0
- package/dist/capabilities/session/extract.js +50 -0
- package/dist/capabilities/session/golden-score.js +114 -0
- package/dist/capabilities/session/health-watch.js +215 -0
- package/dist/capabilities/session/read-guard.js +107 -0
- package/dist/capabilities/session/static-flight-golden.js +150 -0
- package/dist/capabilities/session/transport.js +109 -0
- package/dist/capabilities/session/wizard.js +117 -0
- package/dist/capabilities/session-consent.js +178 -0
- package/dist/capabilities/session-login.js +170 -0
- package/dist/capabilities/session-search.js +686 -0
- package/dist/capabilities/spawn-bounded.js +203 -0
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +140 -0
- package/dist/capabilities/weather.js +243 -138
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +103 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +72 -0
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-error-tests.js +354 -0
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
- package/dist/scripts/anything-tests.js +116 -60
- package/dist/scripts/artifact-client-contract-tests.js +220 -0
- package/dist/scripts/artifacts-capability-tests.js +727 -0
- package/dist/scripts/async-collect.js +68 -10
- package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
- package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
- package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
- package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
- package/dist/scripts/booking-copilot-dsh-warmer-proof.js +123 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
- package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
- package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
- package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
- 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 +125 -0
- package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
- package/dist/scripts/bootstrap-tests.js +1479 -0
- package/dist/scripts/bridge-events-tests.js +239 -0
- package/dist/scripts/bridge-ledger-tests.js +428 -0
- package/dist/scripts/build-changelog.js +238 -0
- package/dist/scripts/build-metrics-report.js +331 -0
- package/dist/scripts/changelog-tests.js +118 -0
- package/dist/scripts/channel-probe-tests.js +103 -0
- package/dist/scripts/channel-probe.js +169 -0
- package/dist/scripts/channel-registry-tests.js +208 -0
- package/dist/scripts/companion-tests.js +67 -0
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-runner-stop-tests.js +311 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +564 -0
- package/dist/scripts/dsh-artifact-e2e.js +279 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
- package/dist/scripts/dsh-runtime-closure.js +162 -0
- package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +539 -0
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +174 -0
- package/dist/scripts/evaluation-contract-tests.js +268 -0
- package/dist/scripts/extension-distribution-cli.js +39 -0
- package/dist/scripts/extension-distribution-tests.js +443 -0
- package/dist/scripts/extension-tests.js +988 -0
- package/dist/scripts/external-event-tests.js +350 -0
- package/dist/scripts/fact-gate-tests.js +869 -0
- 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-setup-tests.js +228 -0
- package/dist/scripts/flyai-setup-tool-tests.js +54 -0
- package/dist/scripts/flyai-tests.js +692 -0
- package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
- package/dist/scripts/fx-contract-tests.js +201 -0
- 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/geo-atlas-tests.js +283 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
- package/dist/scripts/gotry-try-demo-tests.js +79 -0
- package/dist/scripts/gotry-try-demo.js +49 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +159 -0
- 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 +121 -69
- package/dist/scripts/health-watch-cli.js +73 -0
- 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 +41 -0
- 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 +569 -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/issue436-persisted-routing-e2e.js +247 -0
- package/dist/scripts/itinerary-artifact-tests.js +368 -0
- package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
- package/dist/scripts/itinerary-deck-export-tests.js +552 -0
- package/dist/scripts/itinerary-deck-tests.js +435 -0
- package/dist/scripts/itinerary-html-tests.js +426 -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/lavish-local-tests.js +1226 -0
- package/dist/scripts/lavish-product-tools-tests.js +537 -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 +480 -0
- package/dist/scripts/ledger-workflow-crash.js +32 -0
- package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
- package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
- package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
- package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
- package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +49 -0
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +29 -31
- package/dist/scripts/memory-value-report.js +631 -0
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +151 -0
- package/dist/scripts/model-override-e2e.js +137 -0
- package/dist/scripts/nightly-evidence-tests.js +161 -0
- package/dist/scripts/nightly-evidence.js +286 -0
- package/dist/scripts/nudge-digest.js +43 -42
- package/dist/scripts/observability-tests.js +381 -0
- package/dist/scripts/onboarding-tests.js +161 -0
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +49 -0
- package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +340 -0
- package/dist/scripts/price-drift-watch.js +447 -0
- package/dist/scripts/probe-poi-tests.js +53 -45
- package/dist/scripts/product-metrics.js +483 -0
- package/dist/scripts/publish-preverify.js +110 -0
- package/dist/scripts/realtime-pricing-tests.js +76 -0
- package/dist/scripts/recall-tests.js +513 -0
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +18 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +31 -0
- package/dist/scripts/session-attach-poc.js +62 -0
- package/dist/scripts/session-benchmark.js +180 -0
- package/dist/scripts/session-dida-live-e2e.js +199 -0
- package/dist/scripts/session-extract-tests.js +60 -0
- package/dist/scripts/session-link-tests.js +478 -0
- package/dist/scripts/session-login.js +29 -0
- package/dist/scripts/session-tests.js +862 -0
- package/dist/scripts/session-zones-tests.js +358 -0
- package/dist/scripts/sf-live-benchmark.js +367 -0
- package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
- package/dist/scripts/sf-live-cli-tests.js +92 -0
- package/dist/scripts/sf-live-options.js +46 -0
- package/dist/scripts/sf-manifest-tests.js +30 -0
- package/dist/scripts/sf-manifest.js +33 -0
- package/dist/scripts/sf-soft-score-tests.js +67 -0
- package/dist/scripts/sf-summary-tests.js +324 -0
- package/dist/scripts/sf-summary.js +525 -0
- package/dist/scripts/share-tests.js +432 -0
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +28 -11
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +21 -0
- package/dist/scripts/smoke.js +703 -219
- package/dist/scripts/spawn-bounded-abort-tests.js +241 -0
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +261 -0
- package/dist/scripts/state-cli.js +585 -0
- package/dist/scripts/state-repair-cli.js +66 -0
- package/dist/scripts/state-repair-tests.js +140 -0
- package/dist/scripts/static-golden-tests.js +165 -0
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +59 -0
- package/dist/scripts/turn-handoff-collect-tests.js +169 -0
- package/dist/scripts/turn-handoff-collect.js +161 -0
- package/dist/scripts/turn-policy-tests.js +55 -0
- package/dist/scripts/typed-contract-canary.js +176 -0
- 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 +62 -0
- package/dist/scripts/weather-tests.js +276 -41
- package/dist/scripts/wish-channel-gate-tests.js +54 -0
- 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 +41 -0
- package/dist/src/artifact-gate.js +829 -0
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +92 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +551 -0
- package/dist/src/benchmark-agent-conformance.js +578 -0
- package/dist/src/benchmark-environment-bridge.js +515 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
- package/dist/src/benchmark-tool-isolation.js +151 -0
- package/dist/src/bookable-facts.js +533 -0
- package/dist/src/booking-saga.js +97 -0
- package/dist/src/booking-surface/availability-policy.js +697 -0
- package/dist/src/booking-surface/booking-intent.js +43 -0
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/canonical-schema.js +116 -0
- package/dist/src/booking-surface/contracts.js +75 -0
- package/dist/src/booking-surface/dsh-planner.js +1854 -0
- package/dist/src/booking-surface/dsh-plugin.js +378 -0
- package/dist/src/booking-surface/dsh-worker.js +98 -0
- package/dist/src/booking-surface/error-codes.js +231 -0
- package/dist/src/booking-surface/index.js +9 -0
- package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
- package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
- package/dist/src/booking-surface/profile.js +24 -0
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +1968 -0
- package/dist/src/booking-surface/server.js +674 -0
- package/dist/src/booking-surface/startup.js +168 -0
- package/dist/src/booking-surface/validation.js +261 -0
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +86 -0
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +97 -111
- package/dist/src/engine.js +276 -193
- package/dist/src/evaluation-cadence.js +97 -0
- package/dist/src/evaluation-contracts.js +283 -0
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/flyai-setup-tool.js +70 -0
- package/dist/src/gotry-backend.js +125 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +88 -0
- package/dist/src/index.js +2305 -731
- package/dist/src/itinerary-deck.js +139 -0
- package/dist/src/itinerary-doc-shared.js +661 -0
- package/dist/src/itinerary-html.js +131 -0
- package/dist/src/journey.js +155 -108
- package/dist/src/lavish-tools.js +400 -0
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +358 -162
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +47 -0
- 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 +103 -0
- package/dist/src/recall/card.js +74 -0
- package/dist/src/recall/evaluator.js +140 -0
- package/dist/src/recall/integration.js +17 -0
- package/dist/src/recall/tick.js +166 -0
- package/dist/src/session-link/plan-it-action.js +48 -0
- package/dist/src/session-link/session-link.js +248 -0
- package/dist/src/session-zones.js +507 -0
- package/dist/src/share/adapters.js +51 -0
- package/dist/src/share/share-consent.js +76 -0
- package/dist/src/share/share-deck.js +58 -0
- package/dist/src/share/share-token.js +146 -0
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +767 -0
- package/dist/src/state-repair.js +299 -0
- 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 +59 -0
- package/dist/src/turn-deadline.js +287 -0
- package/dist/src/turn-policy.js +118 -0
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +538 -360
- package/dist/src/wish-pool.js +42 -12
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +284 -0
- package/extension/README.md +21 -0
- package/extension/README.zh-CN.md +21 -0
- package/extension/background.js +463 -0
- package/extension/content-bridge.js +270 -0
- package/extension/content-main.js +111 -0
- package/extension/manifest.json +60 -0
- package/package.json +407 -14
- package/schemas/booking.intent.schema.json +193 -0
- package/schemas/booking.surface.schema.json +2622 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +198 -0
- package/ts/capabilities/anything.ts +255 -0
- package/ts/capabilities/artifacts.ts +428 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai-config.ts +388 -0
- package/ts/capabilities/flyai.ts +1274 -0
- package/ts/capabilities/hbcli.ts +253 -71
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/itinerary-artifact.ts +214 -0
- package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
- package/ts/capabilities/itinerary-deck-export.ts +378 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
- package/ts/capabilities/session/benchmark.ts +276 -0
- package/ts/capabilities/session/bridge-ledger.ts +333 -0
- package/ts/capabilities/session/extension-bridge.ts +794 -0
- package/ts/capabilities/session/extension-channel.ts +144 -0
- package/ts/capabilities/session/extension-distribution.ts +266 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +264 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/transport.ts +146 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-consent.ts +211 -0
- package/ts/capabilities/session-login.ts +208 -0
- package/ts/capabilities/session-search.ts +893 -0
- package/ts/capabilities/weather.ts +330 -0
- package/ts/dsh-runtime/vendor/README.md +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
- package/ts/package.json +296 -3
- package/ts/scripts/async-collect.ts +91 -0
- package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
- package/ts/scripts/skeleton-check.ts +1 -1
- package/ts/scripts/skeleton-integration-test.ts +23 -1
- package/ts/scripts/state-cli.ts +568 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +991 -0
- package/ts/src/benchmark-agent-conformance.ts +619 -0
- package/ts/src/benchmark-environment-bridge.ts +538 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/bookable-facts.ts +837 -0
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/booking-surface/availability-policy.ts +522 -0
- package/ts/src/booking-surface/booking-intent.ts +86 -0
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/canonical-schema.js +116 -0
- package/ts/src/booking-surface/contracts.ts +475 -0
- package/ts/src/booking-surface/dsh-planner.ts +1949 -0
- package/ts/src/booking-surface/dsh-plugin.js +378 -0
- package/ts/src/booking-surface/dsh-worker.ts +110 -0
- package/ts/src/booking-surface/error-codes.ts +241 -0
- package/ts/src/booking-surface/index.ts +9 -0
- package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
- package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/runtime.ts +1793 -0
- package/ts/src/booking-surface/server.ts +733 -0
- package/ts/src/booking-surface/startup.ts +201 -0
- package/ts/src/booking-surface/validation.ts +217 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +13 -3
- package/ts/src/dsh-llm.ts +44 -11
- package/ts/src/flyai-setup-tool.ts +77 -0
- package/ts/src/index.ts +2107 -224
- package/ts/src/itinerary-deck.ts +189 -0
- package/ts/src/itinerary-doc-shared.ts +741 -0
- package/ts/src/itinerary-html.ts +171 -0
- package/ts/src/loop.ts +278 -40
- package/ts/src/memory-capture.ts +283 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/recall/card.ts +106 -0
- package/ts/src/recall/evaluator.ts +170 -0
- package/ts/src/recall/integration.ts +35 -0
- package/ts/src/recall/tick.ts +213 -0
- package/ts/src/session-link/plan-it-action.ts +78 -0
- package/ts/src/session-link/session-link.ts +275 -0
- package/ts/src/share/adapters.ts +93 -0
- package/ts/src/share/share-consent.ts +105 -0
- package/ts/src/share/share-deck.ts +85 -0
- package/ts/src/share/share-token.ts +166 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +912 -0
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +338 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/turn-deadline.ts +366 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/unified.ts +360 -89
- package/ts/src/wish-pool.ts +79 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 产物面(issue #285)能力层离线 proof:
|
|
3
|
+
* list/read 工具链的纯函数 + 隔离 fixture 路径护栏。
|
|
4
|
+
* 与 smoke.ts §15 互相补——smoke 验注册 + dsh 卡片契约(在 plugin context),
|
|
5
|
+
* 本文件验 capabilities/artifacts.ts 纯函数 + 隔离 stateRoot(无 dsh 上下文)。
|
|
6
|
+
*
|
|
7
|
+
* 1) 账本权威 list:种工单 + 交付 → list 找到 async-run
|
|
8
|
+
* 2) cwd md list:工作目录顶层 md → list 找到 cwd-file
|
|
9
|
+
* 3) 合并去重:账本 + cwd 同 path 不重复
|
|
10
|
+
* 4) 裸工单 id read:从账本 deliverable 读出
|
|
11
|
+
* 5) 行号窗口:offset/limit 行号正确
|
|
12
|
+
* 6) 越界路径:ok:false + error 含「越界」
|
|
13
|
+
* 7) symlink 越界:ok:false
|
|
14
|
+
* 7b) 裸工单 id symlink/超大 deliverable:仍经过 canonical/root/size 护栏
|
|
15
|
+
* 8) 不支持扩展名:.db → ok:false + error 含「不支持的文件类型」
|
|
16
|
+
* 9) 不存在文件:ok:false
|
|
17
|
+
* 10) 超大文件:>2MB → ok:false
|
|
18
|
+
* 11) read 在 cwd/根边界外的相对路径:rejects
|
|
19
|
+
* 12) list limit 截断:truncated=true, paths.length === limit
|
|
20
|
+
*
|
|
21
|
+
* issue #441 追加(HTML 行程产物发现 + 安全源码预览):
|
|
22
|
+
* 13) cwd 顶层 .html/.htm/大写扩展名同时可发现;标题去扩展名;dotfile 仍排除;md 不退化
|
|
23
|
+
* 14) HTML read:lang=html、原始行号、全文 sha256 指纹、.htm 同 lang
|
|
24
|
+
* 15) HTML 分页:offset/limit 行号 + windowed 语义与 md 一致
|
|
25
|
+
* 16) 恶意 HTML fixture 只作字面文本返回:脚本/内联事件/fetch 不执行、不联网
|
|
26
|
+
* 17) 安全回归:HTML 超大/越界/symlink 逃逸/隐藏文件/拒绝目录,与 md 同限制
|
|
27
|
+
* 18) list→read 实际串联:消费 list 返回的 path 读回 HTML(state-root 与 cwd 两种来源)
|
|
28
|
+
*/
|
|
29
|
+
import assert from 'node:assert/strict';
|
|
30
|
+
import { createHash } from 'node:crypto';
|
|
31
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
32
|
+
import { tmpdir } from 'node:os';
|
|
33
|
+
import { join, resolve } from 'node:path';
|
|
34
|
+
import { listArtifacts, readArtifact } from '../capabilities/artifacts.js';
|
|
35
|
+
import { ensureLedger } from '../src/state-ledger.js';
|
|
36
|
+
const stateRoot = mkdtempSync(join(tmpdir(), 'gotry-artifacts-cap-tests-'));
|
|
37
|
+
const cwd = mkdtempSync(join(tmpdir(), 'gotry-artifacts-cap-cwd-'));
|
|
38
|
+
// 1) 账本权威 list
|
|
39
|
+
{
|
|
40
|
+
const ledger = ensureLedger(stateRoot);
|
|
41
|
+
ledger.createWorkflowRun({ id: 'cap-probe-1', goal: '产物探针·账本', ticket: { objective: 'probe' }, state: {} });
|
|
42
|
+
ledger.settleWorkflowRun('cap-probe-1', '# 交付·账本权威\nD1 大理\nD2 洱海');
|
|
43
|
+
const r = await listArtifacts({ stateRoot, limit: 20 });
|
|
44
|
+
const run = r.artifacts.find(a => a.id === 'cap-probe-1');
|
|
45
|
+
assert.ok(run, '账本工单应被 list 找到');
|
|
46
|
+
assert.equal(run?.source, 'async-run');
|
|
47
|
+
assert.equal(run?.status, 'settled');
|
|
48
|
+
assert.ok(run?.path.endsWith('cap-probe-1.deliverable.md'), `path 形态应为 deliverable.md,实际 ${run?.path}`);
|
|
49
|
+
console.log('1) 账本权威 list OK');
|
|
50
|
+
}
|
|
51
|
+
// 2) cwd md list
|
|
52
|
+
{
|
|
53
|
+
const lines = Array.from({ length: 8 }, (_, i) => `第 ${i + 1} 行`);
|
|
54
|
+
writeFileSync(join(cwd, 'trip-2027-cap.md'), `# 行程探针\n${lines.join('\n')}\n`);
|
|
55
|
+
const r = await listArtifacts({ stateRoot, cwd, limit: 20 });
|
|
56
|
+
const md = r.artifacts.find(a => a.id === 'trip-2027-cap.md');
|
|
57
|
+
assert.ok(md, 'cwd 顶层 md 应被 list 找到');
|
|
58
|
+
assert.equal(md?.source, 'cwd-file');
|
|
59
|
+
console.log('2) cwd md list OK');
|
|
60
|
+
}
|
|
61
|
+
// 3) 合并去重:账本工单 cap-probe-1 的 path 与 cwd 任何文件 path 不冲突——验证 seenPath 去重逻辑
|
|
62
|
+
{
|
|
63
|
+
// 制造一个 cwd 文件与账本 deliverable 同 basename 看似不同
|
|
64
|
+
writeFileSync(join(cwd, 'cap-probe-1.deliverable.md'), 'unrelated');
|
|
65
|
+
const r = await listArtifacts({ stateRoot, cwd, limit: 20 });
|
|
66
|
+
const seen = new Map();
|
|
67
|
+
for (const a of r.artifacts) {
|
|
68
|
+
if (a.id === 'cap-probe-1.deliverable.md' || a.id === 'cap-probe-1') {
|
|
69
|
+
const k = resolve(a.path);
|
|
70
|
+
seen.set(k, (seen.get(k) ?? 0) + 1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// 同一 path 出现 ≥2 次即去重失败
|
|
74
|
+
for (const [k, n] of seen) {
|
|
75
|
+
assert.ok(n <= 1, `path ${k} 出现 ${n} 次,应去重为 1`);
|
|
76
|
+
}
|
|
77
|
+
console.log('3) 合并去重 OK');
|
|
78
|
+
}
|
|
79
|
+
// 4) 裸工单 id read
|
|
80
|
+
{
|
|
81
|
+
const r = await readArtifact({ stateRoot, cwd, path: 'cap-probe-1' });
|
|
82
|
+
assert.equal(r.ok, true, '裸工单 id 应可读');
|
|
83
|
+
if (r.ok) {
|
|
84
|
+
assert.equal(r.lang, 'markdown');
|
|
85
|
+
assert.equal(r.offset, 1);
|
|
86
|
+
assert.equal(r.lines[0]?.number, 1);
|
|
87
|
+
assert.ok(r.lines[0]?.text.includes('交付·账本权威'), '首行应含账本 deliverable 标题');
|
|
88
|
+
assert.equal(r.totalLines, 3, `交付 3 行,实际 ${r.totalLines}`);
|
|
89
|
+
}
|
|
90
|
+
console.log('4) 裸工单 id read OK');
|
|
91
|
+
}
|
|
92
|
+
// 5) 行号窗口
|
|
93
|
+
{
|
|
94
|
+
const r = await readArtifact({ stateRoot, cwd, path: 'trip-2027-cap.md', offset: 3, limit: 3 });
|
|
95
|
+
assert.equal(r.ok, true, 'cwd md 应可读');
|
|
96
|
+
if (r.ok) {
|
|
97
|
+
assert.equal(r.lines.length, 3, `offset=3 limit=3 应 3 行,实际 ${r.lines.length}`);
|
|
98
|
+
assert.equal(r.lines[0]?.number, 3, `首行号应 3,实际 ${r.lines[0]?.number}`);
|
|
99
|
+
assert.equal(r.lines[2]?.number, 5);
|
|
100
|
+
assert.equal(r.totalLines, 10, `8 行 + 标题 1 + 末尾空行 1 = 10,实际 ${r.totalLines}`);
|
|
101
|
+
assert.equal(r.windowed, true, `offset=3 limit=3 总 10 行 → windowed=true(可继续翻页)`);
|
|
102
|
+
}
|
|
103
|
+
const full = await readArtifact({ stateRoot, cwd, path: 'trip-2027-cap.md' });
|
|
104
|
+
assert.equal(full.ok, true, 'cwd md 默认 limit 应读完');
|
|
105
|
+
if (full.ok) {
|
|
106
|
+
assert.equal(full.windowed, false, `读完应 windowed=false`);
|
|
107
|
+
assert.equal(full.lines.length, full.totalLines, `lines.length === totalLines,实际 ${full.lines.length}/${full.totalLines}`);
|
|
108
|
+
}
|
|
109
|
+
console.log('5) 行号窗口 OK');
|
|
110
|
+
}
|
|
111
|
+
// 6) 越界路径
|
|
112
|
+
{
|
|
113
|
+
const r = await readArtifact({ stateRoot, cwd, path: '../../../../../etc/passwd' });
|
|
114
|
+
assert.equal(r.ok, false, '越界路径必须被拒');
|
|
115
|
+
if (!r.ok) {
|
|
116
|
+
assert.ok(r.error.includes('越界'), `error 应含「越界」,实际 ${r.error}`);
|
|
117
|
+
}
|
|
118
|
+
console.log('6) 越界路径 OK');
|
|
119
|
+
}
|
|
120
|
+
// 7) symlink 越界
|
|
121
|
+
{
|
|
122
|
+
const linkPath = join(cwd, 'symlink-out.md');
|
|
123
|
+
try {
|
|
124
|
+
symlinkSync('/etc/passwd', linkPath);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// macOS 上 /etc/passwd 是符号链接到 /private/etc/passwd;symlink 自身仍 out-of-root
|
|
128
|
+
}
|
|
129
|
+
const r = await readArtifact({ stateRoot, cwd, path: 'symlink-out.md' });
|
|
130
|
+
assert.equal(r.ok, false, '相对路径 symlink 解析后越出 cwd 必须被拒');
|
|
131
|
+
if (!r.ok)
|
|
132
|
+
assert.ok(r.error.includes('越界'), `symlink 越界错误应含「越界」,实际 ${r.error}`);
|
|
133
|
+
// 绝对 symlink 指向目录也必须按 canonical target 做边界检查。
|
|
134
|
+
const absLink = join(stateRoot, 'abs-link.md');
|
|
135
|
+
try {
|
|
136
|
+
symlinkSync('/etc', absLink);
|
|
137
|
+
const r2 = await readArtifact({ stateRoot, cwd, path: absLink });
|
|
138
|
+
assert.equal(r2.ok, false, '绝对 symlink 越界必须被拒');
|
|
139
|
+
if (!r2.ok) {
|
|
140
|
+
assert.ok(r2.error.includes('越界'), `error 应含「越界」,实际 ${r2.error}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch { /* 非 POSIX fs 跳过 */ }
|
|
144
|
+
console.log('7) symlink 越界 OK');
|
|
145
|
+
}
|
|
146
|
+
// 7b) 裸工单 id 的兼容文件视图也必须经过同一组 canonical/root/size 护栏。
|
|
147
|
+
{
|
|
148
|
+
mkdirSync(join(stateRoot, 'gotry-state', 'async'), { recursive: true });
|
|
149
|
+
const bareLink = join(stateRoot, 'gotry-state', 'async', 'evil.deliverable.md');
|
|
150
|
+
let bareLinkCreated = false;
|
|
151
|
+
try {
|
|
152
|
+
symlinkSync('/etc/hosts', bareLink);
|
|
153
|
+
bareLinkCreated = true;
|
|
154
|
+
}
|
|
155
|
+
catch { /* 非 POSIX fs 跳过 */ }
|
|
156
|
+
if (bareLinkCreated) {
|
|
157
|
+
const r = await readArtifact({ stateRoot, cwd, path: 'evil' });
|
|
158
|
+
assert.equal(r.ok, false, '裸工单 id symlink 指向 root 外时必须被拒');
|
|
159
|
+
if (!r.ok)
|
|
160
|
+
assert.ok(r.error.includes('越界'), `裸工单 id symlink 越界错误应含「越界」,实际 ${r.error}`);
|
|
161
|
+
}
|
|
162
|
+
const bareBig = join(stateRoot, 'gotry-state', 'async', 'oversized.deliverable.md');
|
|
163
|
+
writeFileSync(bareBig, 'x'.repeat(2 * 1024 * 1024 + 1));
|
|
164
|
+
const oversized = await readArtifact({ stateRoot, cwd, path: 'oversized' });
|
|
165
|
+
assert.equal(oversized.ok, false, '裸工单 id 超大文件必须被拒');
|
|
166
|
+
if (!oversized.ok)
|
|
167
|
+
assert.ok(oversized.error.includes('过大'), `裸工单 id 超大错误应含「过大」,实际 ${oversized.error}`);
|
|
168
|
+
rmSync(bareLink, { force: true });
|
|
169
|
+
rmSync(bareBig, { force: true });
|
|
170
|
+
console.log('7b) 裸工单 id symlink/超大 deliverable 护栏 OK');
|
|
171
|
+
}
|
|
172
|
+
// 8) 不支持扩展名
|
|
173
|
+
{
|
|
174
|
+
mkdirSync(join(stateRoot, 'gotry-state'), { recursive: true });
|
|
175
|
+
writeFileSync(join(stateRoot, 'gotry-state', 'gotry-state.db'), 'fake db');
|
|
176
|
+
const r = await readArtifact({ stateRoot, cwd, path: 'gotry-state/gotry-state.db' });
|
|
177
|
+
assert.equal(r.ok, false, '.db 必须被拒');
|
|
178
|
+
if (!r.ok) {
|
|
179
|
+
assert.ok(r.error.includes('不支持的文件类型'), `error 应含「不支持的文件类型」,实际 ${r.error}`);
|
|
180
|
+
}
|
|
181
|
+
console.log('8) 不支持扩展名 OK');
|
|
182
|
+
}
|
|
183
|
+
// 9) 不存在文件
|
|
184
|
+
{
|
|
185
|
+
const r = await readArtifact({ stateRoot, cwd, path: 'not-found.md' });
|
|
186
|
+
assert.equal(r.ok, false, '不存在文件必须被拒');
|
|
187
|
+
if (!r.ok) {
|
|
188
|
+
assert.ok(r.error.includes('不存在'), `error 应含「不存在」,实际 ${r.error}`);
|
|
189
|
+
}
|
|
190
|
+
console.log('9) 不存在文件 OK');
|
|
191
|
+
}
|
|
192
|
+
// 10) 超大文件
|
|
193
|
+
{
|
|
194
|
+
const big = join(cwd, 'big.md');
|
|
195
|
+
// 2MB+ 一点点;read 默认上限 2MB
|
|
196
|
+
const chunk = 'x'.repeat(64 * 1024);
|
|
197
|
+
const handle = await import('node:fs/promises').then(m => m.open(big, 'w'));
|
|
198
|
+
try {
|
|
199
|
+
for (let i = 0; i < 33; i++)
|
|
200
|
+
await handle.write(chunk); // 33 * 64KB = 2.11MB
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
await handle.close();
|
|
204
|
+
}
|
|
205
|
+
const r = await readArtifact({ stateRoot, cwd, path: 'big.md' });
|
|
206
|
+
assert.equal(r.ok, false, '>2MB 必须被拒');
|
|
207
|
+
if (!r.ok) {
|
|
208
|
+
assert.ok(r.error.includes('过大'), `error 应含「过大」,实际 ${r.error}`);
|
|
209
|
+
}
|
|
210
|
+
console.log('10) 超大文件 OK');
|
|
211
|
+
}
|
|
212
|
+
// 11) read 接受 path 三形态:id / cwd 文件名 / 绝对路径
|
|
213
|
+
{
|
|
214
|
+
// 11a) 相对文件名仅在 cwd 顶层
|
|
215
|
+
const r = await readArtifact({ stateRoot, cwd, path: 'trip-2027-cap.md' });
|
|
216
|
+
assert.equal(r.ok, true, '相对文件名应解析为 cwd 顶层');
|
|
217
|
+
// 11b) path 为裸字符串无后缀 + 不在 cwd → 尝试 stateRoot 下的 gotry-state/async/
|
|
218
|
+
const r2 = await readArtifact({ stateRoot, cwd, path: 'cap-probe-1' });
|
|
219
|
+
assert.equal(r2.ok, true, '裸工单 id 应解析为账本');
|
|
220
|
+
// 11c) 空 path
|
|
221
|
+
const r3 = await readArtifact({ stateRoot, cwd, path: '' });
|
|
222
|
+
assert.equal(r3.ok, false, '空 path 应被拒');
|
|
223
|
+
console.log('11) path 三形态 OK');
|
|
224
|
+
}
|
|
225
|
+
// 12) list limit 截断
|
|
226
|
+
{
|
|
227
|
+
for (let i = 0; i < 5; i++)
|
|
228
|
+
writeFileSync(join(cwd, `extra-${i}.md`), `# 副产物 ${i}`);
|
|
229
|
+
const r = await listArtifacts({ stateRoot, cwd, limit: 3 });
|
|
230
|
+
assert.equal(r.artifacts.length, 3, `limit=3 应 3 条,实际 ${r.artifacts.length}`);
|
|
231
|
+
assert.equal(r.truncated, true, `应有更多,truncated 应为 true`);
|
|
232
|
+
// 自 #458 起 list 不再按源提前 limit,total 反映已过滤集合的全部条目。
|
|
233
|
+
// 此时 ledger 1(cap-probe-1)+ cwd 顶层 8 个(trip-2027-cap.md +
|
|
234
|
+
// cap-probe-1.deliverable.md + big.md + extra-0..4)= 9。test 13+ 才加 HTML/htm/
|
|
235
|
+
// 隐藏文件,不影响本断言。
|
|
236
|
+
assert.equal(r.total, 9, `total 应 9,实际 ${r.total}`);
|
|
237
|
+
assert.ok(r.total > r.artifacts.length, `total 应 > artifacts.length 才能 truncated,实际 ${r.total} vs ${r.artifacts.length}`);
|
|
238
|
+
assert.equal(r.nextOffset, 3, `limit=3 truncated 应 nextOffset=3,实际 ${r.nextOffset}`);
|
|
239
|
+
// 放大 limit 后能看到全部
|
|
240
|
+
const r2 = await listArtifacts({ stateRoot, cwd, limit: 20 });
|
|
241
|
+
assert.equal(r2.truncated, false, `limit=20 应能装下,truncated=false`);
|
|
242
|
+
assert.ok(r2.total >= 9, `放大 limit 后 total 应 ≥ 9,实际 ${r2.total}`);
|
|
243
|
+
console.log('12) list limit 截断 OK');
|
|
244
|
+
}
|
|
245
|
+
// ============ issue #441:HTML 行程产物 ============
|
|
246
|
+
const HTML_TEXT = [
|
|
247
|
+
'<!doctype html>',
|
|
248
|
+
'<html lang="zh-CN"><head><meta charset="utf-8"><title>合成行程·大理</title></head>',
|
|
249
|
+
'<body>',
|
|
250
|
+
'<h1>合成行程 2027</h1>',
|
|
251
|
+
'<p>D1 大理 · D2 洱海</p>',
|
|
252
|
+
'</body></html>',
|
|
253
|
+
'',
|
|
254
|
+
].join('\n');
|
|
255
|
+
// 13) cwd 顶层 html/htm 发现(大小写不敏感)+ 标题 + dotfile 排除 + md 不退化
|
|
256
|
+
{
|
|
257
|
+
writeFileSync(join(cwd, 'trip-2027-cap.html'), HTML_TEXT);
|
|
258
|
+
writeFileSync(join(cwd, 'trip-2027-cap.htm'), '<html><body><p>htm 变体</p></body></html>');
|
|
259
|
+
writeFileSync(join(cwd, 'Trip-2027-CAPS.HTML'), '<html><body><p>大写扩展名</p></body></html>');
|
|
260
|
+
writeFileSync(join(cwd, '.hidden-trip.html'), '<html><body><p>隐藏文件不得发现</p></body></html>');
|
|
261
|
+
const r = await listArtifacts({ stateRoot, cwd, limit: 50 });
|
|
262
|
+
const ids = r.artifacts.map(a => a.id);
|
|
263
|
+
for (const name of ['trip-2027-cap.html', 'trip-2027-cap.htm', 'Trip-2027-CAPS.HTML']) {
|
|
264
|
+
assert.ok(ids.includes(name), `顶层 ${name} 应被 list 发现,实际 ${ids.join(',')}`);
|
|
265
|
+
}
|
|
266
|
+
assert.ok(ids.includes('trip-2027-cap.md'), `md 发现不得退化,实际 ${ids.join(',')}`);
|
|
267
|
+
assert.ok(!ids.includes('.hidden-trip.html'), `dotfile 仍应被排除,实际 ${ids.join(',')}`);
|
|
268
|
+
const html = r.artifacts.find(a => a.id === 'trip-2027-cap.html');
|
|
269
|
+
assert.equal(html?.source, 'cwd-file');
|
|
270
|
+
assert.equal(html?.title, 'trip-2027-cap', `标题应去扩展名,实际 ${html?.title}`);
|
|
271
|
+
assert.equal(html?.bytes, Buffer.byteLength(HTML_TEXT), `bytes 应为文件字节数,实际 ${html?.bytes}`);
|
|
272
|
+
const caps = r.artifacts.find(a => a.id === 'Trip-2027-CAPS.HTML');
|
|
273
|
+
assert.equal(caps?.title, 'Trip-2027-CAPS', `大写扩展名同样去扩展名,实际 ${caps?.title}`);
|
|
274
|
+
// 排序仍是 mtime 倒序 + limit 截断(total 为各源保留数之和,与 md 语义一致)
|
|
275
|
+
const limited = await listArtifacts({ stateRoot, cwd, limit: 2 });
|
|
276
|
+
assert.equal(limited.artifacts.length, 2, `limit=2 应 2 条,实际 ${limited.artifacts.length}`);
|
|
277
|
+
assert.equal(limited.truncated, true, 'limit=2 应截断');
|
|
278
|
+
// 排序单调性:mtime 倒序(updated 字符串倒序)
|
|
279
|
+
const all = await listArtifacts({ stateRoot, cwd, limit: 50 });
|
|
280
|
+
for (let i = 1; i < all.artifacts.length; i++) {
|
|
281
|
+
const prev = String(all.artifacts[i - 1]?.updated ?? '');
|
|
282
|
+
const cur = String(all.artifacts[i]?.updated ?? '');
|
|
283
|
+
assert.ok(prev.localeCompare(cur) >= 0, `排序应 mtime 倒序:${prev} < ${cur}`);
|
|
284
|
+
}
|
|
285
|
+
console.log('13) cwd html/htm 发现 + 标题/排序/截断 OK');
|
|
286
|
+
}
|
|
287
|
+
// 14) HTML read:lang=html + 原始行号 + 全文指纹(.htm 同 lang)
|
|
288
|
+
{
|
|
289
|
+
const r = await readArtifact({ stateRoot, cwd, path: 'trip-2027-cap.html' });
|
|
290
|
+
assert.equal(r.ok, true, 'cwd html 应可读');
|
|
291
|
+
if (r.ok) {
|
|
292
|
+
assert.equal(r.lang, 'html', `lang 应 html,实际 ${r.lang}`);
|
|
293
|
+
assert.equal(r.source, 'cwd', `source 应 cwd,实际 ${r.source}`);
|
|
294
|
+
assert.equal(r.totalLines, 7, `HTML_TEXT 7 行,实际 ${r.totalLines}`);
|
|
295
|
+
assert.equal(r.lines[0]?.number, 1, '首行号应 1');
|
|
296
|
+
assert.equal(r.lines[3]?.text, '<h1>合成行程 2027</h1>', `第 4 行原文应保真,实际 ${r.lines[3]?.text}`);
|
|
297
|
+
const expected = createHash('sha256').update(HTML_TEXT).digest('hex').slice(0, 12);
|
|
298
|
+
assert.equal(r.version, expected, `版本应为全文 sha256 前 12 位,实际 ${r.version}`);
|
|
299
|
+
assert.equal(r.windowed, false, '全文一次读完应 windowed=false');
|
|
300
|
+
}
|
|
301
|
+
const caps = await readArtifact({ stateRoot, cwd, path: 'Trip-2027-CAPS.HTML' });
|
|
302
|
+
assert.equal(caps.ok, true, '大写 .HTML 应可读');
|
|
303
|
+
if (caps.ok)
|
|
304
|
+
assert.equal(caps.lang, 'html', `大写扩展名 lang 仍 html,实际 ${caps.lang}`);
|
|
305
|
+
const htm = await readArtifact({ stateRoot, cwd, path: 'trip-2027-cap.htm' });
|
|
306
|
+
assert.equal(htm.ok, true, '.htm 应可读');
|
|
307
|
+
if (htm.ok) {
|
|
308
|
+
assert.equal(htm.lang, 'html', `.htm lang 应 html,实际 ${htm.lang}`);
|
|
309
|
+
assert.ok(htm.content.includes('<p>htm 变体</p>'), '.htm 内容应原文返回');
|
|
310
|
+
}
|
|
311
|
+
console.log('14) HTML read lang/行号/指纹 OK');
|
|
312
|
+
}
|
|
313
|
+
// 15) HTML 分页与 md 同语义
|
|
314
|
+
{
|
|
315
|
+
const r = await readArtifact({ stateRoot, cwd, path: 'trip-2027-cap.html', offset: 3, limit: 2 });
|
|
316
|
+
assert.equal(r.ok, true, 'HTML 分页应可读');
|
|
317
|
+
if (r.ok) {
|
|
318
|
+
assert.equal(r.lines.length, 2, `offset=3 limit=2 应 2 行,实际 ${r.lines.length}`);
|
|
319
|
+
assert.equal(r.lines[0]?.number, 3, `首行号应 3,实际 ${r.lines[0]?.number}`);
|
|
320
|
+
assert.equal(r.lines[1]?.number, 4);
|
|
321
|
+
assert.equal(r.offset, 3);
|
|
322
|
+
assert.equal(r.windowed, true, '总 7 行读 3-4 行应 windowed=true');
|
|
323
|
+
}
|
|
324
|
+
console.log('15) HTML 分页 OK');
|
|
325
|
+
}
|
|
326
|
+
// 16) 恶意 HTML fixture:只作字面文本返回,不执行脚本/内联事件,不发网络请求
|
|
327
|
+
{
|
|
328
|
+
const evil = [
|
|
329
|
+
'<!doctype html><html><body>',
|
|
330
|
+
'<script>globalThis.__gotry441Pwned = true</script>',
|
|
331
|
+
'<img src=x onerror="globalThis.__gotry441Pwned = true">',
|
|
332
|
+
'<iframe src="https://example.invalid/track"></iframe>',
|
|
333
|
+
'<a href="javascript:globalThis.__gotry441Pwned=true">点击</a>',
|
|
334
|
+
'<script>fetch("http://127.0.0.1:1/collect")</script>',
|
|
335
|
+
'</body></html>',
|
|
336
|
+
'',
|
|
337
|
+
].join('\n');
|
|
338
|
+
writeFileSync(join(cwd, 'evil-itinerary.html'), evil);
|
|
339
|
+
const r = await readArtifact({ stateRoot, cwd, path: 'evil-itinerary.html', limit: 400 });
|
|
340
|
+
assert.equal(r.ok, true, '恶意 HTML 仍应作为文本可读');
|
|
341
|
+
if (r.ok) {
|
|
342
|
+
const content = r.lines.map(l => l.text).join('\n');
|
|
343
|
+
assert.ok(content.includes('<script>globalThis.__gotry441Pwned = true</script>'), '脚本应原样作为文本返回');
|
|
344
|
+
assert.ok(content.includes('onerror="globalThis.__gotry441Pwned = true"'), '内联事件属性应原样作为文本返回');
|
|
345
|
+
assert.ok(content.includes('javascript:globalThis.__gotry441Pwned=true'), 'javascript: 链接应原样作为文本返回');
|
|
346
|
+
assert.ok(content.includes('fetch("http://127.0.0.1:1/collect")'), 'fetch 调用应原样作为文本返回');
|
|
347
|
+
assert.ok(r.lines.every(l => typeof l.text === 'string'), '所有行必须是纯字符串文本');
|
|
348
|
+
assert.equal(r.lang, 'html', 'lang 仍是 html(源码预览,非渲染)');
|
|
349
|
+
assert.equal(r.version, createHash('sha256').update(evil).digest('hex').slice(0, 12), '指纹基于原文文本');
|
|
350
|
+
}
|
|
351
|
+
assert.equal(globalThis.__gotry441Pwned, undefined, '读取过程不得执行 HTML 内脚本');
|
|
352
|
+
console.log('16) 恶意 HTML 字面文本 + 零执行 OK');
|
|
353
|
+
}
|
|
354
|
+
// 17) 安全回归:HTML 与 md 同限制(超大/越界/symlink 逃逸/隐藏/拒绝目录)
|
|
355
|
+
{
|
|
356
|
+
// 17a) 超大 HTML(>2MB)
|
|
357
|
+
const bigHtml = join(cwd, 'big-itinerary.html');
|
|
358
|
+
const chunk = '<p>xxxx</p>'.repeat(64 * 1024);
|
|
359
|
+
const handle = await import('node:fs/promises').then(m => m.open(bigHtml, 'w'));
|
|
360
|
+
try {
|
|
361
|
+
for (let i = 0; i < 4; i++)
|
|
362
|
+
await handle.write(chunk); // 4 * 640KB = 2.6MB > 2MB
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
await handle.close();
|
|
366
|
+
}
|
|
367
|
+
const big = await readArtifact({ stateRoot, cwd, path: 'big-itinerary.html' });
|
|
368
|
+
assert.equal(big.ok, false, '>2MB HTML 必须被拒');
|
|
369
|
+
if (!big.ok)
|
|
370
|
+
assert.ok(big.error.includes('过大'), `超大 HTML 错误应含「过大」,实际 ${big.error}`);
|
|
371
|
+
// 17b) 越界相对路径(html 扩展名不豁免边界)
|
|
372
|
+
const out = await readArtifact({ stateRoot, cwd, path: '../../../../etc/passwd.html' });
|
|
373
|
+
assert.equal(out.ok, false, '越界 html 必须被拒');
|
|
374
|
+
if (!out.ok)
|
|
375
|
+
assert.ok(out.error.includes('越界'), `越界错误应含「越界」,实际 ${out.error}`);
|
|
376
|
+
// 17c) symlink 逃逸:指向 root 外的 .html 既不可 list 也不可 read
|
|
377
|
+
const linkHtml = join(cwd, 'symlink-evil.html');
|
|
378
|
+
try {
|
|
379
|
+
symlinkSync('/etc/passwd', linkHtml);
|
|
380
|
+
const listed = await listArtifacts({ stateRoot, cwd, limit: 50 });
|
|
381
|
+
assert.ok(!listed.artifacts.some(a => a.id === 'symlink-evil.html'), 'symlink 文件不得进入 list');
|
|
382
|
+
const read = await readArtifact({ stateRoot, cwd, path: 'symlink-evil.html' });
|
|
383
|
+
assert.equal(read.ok, false, 'symlink 逃逸的 html 必须被拒');
|
|
384
|
+
if (!read.ok)
|
|
385
|
+
assert.ok(read.error.includes('越界'), `symlink 逃逸错误应含「越界」,实际 ${read.error}`);
|
|
386
|
+
}
|
|
387
|
+
catch { /* 非 POSIX fs 跳过 */ }
|
|
388
|
+
// 17d) 隐藏文件:list 排除(同 md);显式 path 读取行为与 md 一致(不新增限制)
|
|
389
|
+
writeFileSync(join(cwd, '.hidden-parity.md'), 'md 隐藏');
|
|
390
|
+
const hiddenHtml = await readArtifact({ stateRoot, cwd, path: '.hidden-trip.html' });
|
|
391
|
+
const hiddenMd = await readArtifact({ stateRoot, cwd, path: '.hidden-parity.md' });
|
|
392
|
+
assert.equal(hiddenHtml.ok, hiddenMd.ok, '.html 与 .md 隐藏文件的显式读取行为必须一致');
|
|
393
|
+
const listed2 = await listArtifacts({ stateRoot, cwd, limit: 50 });
|
|
394
|
+
assert.ok(!listed2.artifacts.some(a => a.id === '.hidden-parity.md'), 'md dotfile 同样不得进入 list');
|
|
395
|
+
// 17e) 拒绝目录:node_modules 内 html 既不可读也不可发现
|
|
396
|
+
mkdirSync(join(cwd, 'node_modules'), { recursive: true });
|
|
397
|
+
writeFileSync(join(cwd, 'node_modules', 'vendored.html'), '<html><body>vendored</body></html>');
|
|
398
|
+
const denied = await readArtifact({ stateRoot, cwd, path: 'node_modules/vendored.html' });
|
|
399
|
+
assert.equal(denied.ok, false, 'node_modules 内 html 必须被拒');
|
|
400
|
+
if (!denied.ok)
|
|
401
|
+
assert.ok(denied.error.includes('越界'), `拒绝目录错误应含「越界」,实际 ${denied.error}`);
|
|
402
|
+
const listed3 = await listArtifacts({ stateRoot, cwd, limit: 50 });
|
|
403
|
+
assert.ok(!listed3.artifacts.some(a => a.id === 'vendored.html'), 'node_modules 内文件不得进入 list(非递归)');
|
|
404
|
+
rmSync(join(cwd, 'node_modules'), { recursive: true, force: true });
|
|
405
|
+
console.log('17) HTML 安全回归(超大/越界/symlink/隐藏/拒绝目录)OK');
|
|
406
|
+
}
|
|
407
|
+
// 18) list→read 实际串联:消费 list 返回的 path(绝对路径形态)读回 HTML
|
|
408
|
+
{
|
|
409
|
+
const rootHtml = join(stateRoot, 'gotry-state', 'trip-in-root.html');
|
|
410
|
+
mkdirSync(join(stateRoot, 'gotry-state'), { recursive: true });
|
|
411
|
+
writeFileSync(rootHtml, '<html><body><p>state-root 内 HTML</p></body></html>');
|
|
412
|
+
const listed = await listArtifacts({ stateRoot, cwd, limit: 50 });
|
|
413
|
+
const cwdEntry = listed.artifacts.find(a => a.id === 'trip-2027-cap.html');
|
|
414
|
+
assert.ok(cwdEntry, 'list 应返回 cwd html 条目');
|
|
415
|
+
const viaList = await readArtifact({ stateRoot, cwd, path: String(cwdEntry?.path) });
|
|
416
|
+
assert.equal(viaList.ok, true, 'list 返回的 cwd 绝对路径应可读');
|
|
417
|
+
if (viaList.ok) {
|
|
418
|
+
assert.equal(viaList.lang, 'html');
|
|
419
|
+
assert.equal(viaList.source, 'cwd', `cwd 来源标签,实际 ${viaList.source}`);
|
|
420
|
+
}
|
|
421
|
+
const viaRoot = await readArtifact({ stateRoot, cwd, path: rootHtml });
|
|
422
|
+
assert.equal(viaRoot.ok, true, 'stateRoot 内 HTML 应可读');
|
|
423
|
+
if (viaRoot.ok) {
|
|
424
|
+
assert.equal(viaRoot.lang, 'html');
|
|
425
|
+
assert.equal(viaRoot.source, 'state-root', `state-root 来源标签,实际 ${viaRoot.source}`);
|
|
426
|
+
assert.ok(viaRoot.content.includes('state-root 内 HTML'), '内容应为原文');
|
|
427
|
+
}
|
|
428
|
+
// 读回内容与磁盘原文逐字一致(不做任何 HTML 解析/改写)
|
|
429
|
+
const onDisk = readFileSync(join(cwd, 'trip-2027-cap.html'), 'utf8');
|
|
430
|
+
const roundTrip = await readArtifact({ stateRoot, cwd, path: 'trip-2027-cap.html' });
|
|
431
|
+
if (roundTrip.ok)
|
|
432
|
+
assert.equal(roundTrip.content, onDisk, '读回内容必须与磁盘原文逐字一致');
|
|
433
|
+
console.log('18) list→read 串联(state-root/cwd)OK');
|
|
434
|
+
}
|
|
435
|
+
// ============ issue #458:list paging + literal metadata search ============
|
|
436
|
+
// 19) >50 cwd entries: enumerated across pages with no omission/duplicate
|
|
437
|
+
{
|
|
438
|
+
const bigRoot = mkdtempSync(join(tmpdir(), 'gotry-458-big-root-'));
|
|
439
|
+
const bigCwd = mkdtempSync(join(tmpdir(), 'gotry-458-big-'));
|
|
440
|
+
try {
|
|
441
|
+
const ids = [];
|
|
442
|
+
// 60 timestamped md files, oldest mtime first so updated ASC == alphabetical.
|
|
443
|
+
for (let i = 0; i < 60; i++) {
|
|
444
|
+
const name = `itinerary-${String(i).padStart(3, '0')}.md`;
|
|
445
|
+
const p = join(bigCwd, name);
|
|
446
|
+
writeFileSync(p, `# itinerary ${i}\n`);
|
|
447
|
+
// Older = smaller i; mtime strictly increasing ⇒ updated ASC mirrors id ASC.
|
|
448
|
+
const ts = new Date(Date.UTC(2026, 0, 1, 0, 0, i));
|
|
449
|
+
const { utimesSync } = await import('node:fs');
|
|
450
|
+
utimesSync(p, ts, ts);
|
|
451
|
+
ids.push(name);
|
|
452
|
+
}
|
|
453
|
+
const r1 = await listArtifacts({ stateRoot: bigRoot, cwd: bigCwd, limit: 20 });
|
|
454
|
+
assert.equal(r1.artifacts.length, 20, `default page 应 20 条,实际 ${r1.artifacts.length}`);
|
|
455
|
+
assert.equal(r1.total, 60, `total 应 60(全部 cwd 顶层),实际 ${r1.total}`);
|
|
456
|
+
assert.equal(r1.truncated, true, `truncated 应 true`);
|
|
457
|
+
assert.equal(r1.nextOffset, 20, `nextOffset 应 20,实际 ${r1.nextOffset}`);
|
|
458
|
+
assert.equal(r1.limit, 20, `limit echo 应 20,实际 ${r1.limit}`);
|
|
459
|
+
assert.equal(r1.offset, 0, `offset echo 应 0`);
|
|
460
|
+
const r2 = await listArtifacts({ stateRoot: bigRoot, cwd: bigCwd, limit: 9999 });
|
|
461
|
+
assert.equal(r2.artifacts.length, 50, `limit 9999 应被夹到 50,实际 ${r2.artifacts.length}`);
|
|
462
|
+
assert.equal(r2.total, 60, `cap=50 时 total 仍应 60(不被截断到 50),实际 ${r2.total}`);
|
|
463
|
+
assert.equal(r2.truncated, true, `50<60 时 truncated 应 true`);
|
|
464
|
+
assert.equal(r2.limit, 50, `limit echo 应 50`);
|
|
465
|
+
assert.equal(r2.nextOffset, 50, `nextOffset 应 50`);
|
|
466
|
+
// 全 60 条刚好分两页半:page1+page2 完整覆盖,最后 10 条从 page2 起
|
|
467
|
+
const page1 = await listArtifacts({ stateRoot: bigRoot, cwd: bigCwd, limit: 30, offset: 0 });
|
|
468
|
+
const page2 = await listArtifacts({ stateRoot: bigRoot, cwd: bigCwd, limit: 30, offset: 30 });
|
|
469
|
+
const page3 = await listArtifacts({ stateRoot: bigRoot, cwd: bigCwd, limit: 30, offset: 60 });
|
|
470
|
+
const collected = new Set();
|
|
471
|
+
for (const p of [page1, page2, page3]) {
|
|
472
|
+
assert.equal(p.total, 60, `各页 total 都应 60,实际 ${p.total}`);
|
|
473
|
+
for (const a of p.artifacts) {
|
|
474
|
+
assert.equal(collected.has(a.path), false, `path ${a.path} 在分页中出现多次`);
|
|
475
|
+
collected.add(a.path);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
assert.equal(collected.size, 60, `三页拼起来必须唯一覆盖 60 条,实际 ${collected.size}`);
|
|
479
|
+
for (const id of ids) {
|
|
480
|
+
assert.ok([...collected].some(p => p.endsWith(join(bigCwd, id))), `应能找到 ${id}`);
|
|
481
|
+
}
|
|
482
|
+
assert.equal(page1.nextOffset, 30, `page1 nextOffset 应 30`);
|
|
483
|
+
assert.equal(page2.nextOffset, undefined, `page2 已是最后一页,无 nextOffset`);
|
|
484
|
+
assert.equal(page2.truncated, false, `page2 truncated 应 false`);
|
|
485
|
+
assert.equal(page3.artifacts.length, 0, `越界页应空,实际 ${page3.artifacts.length}`);
|
|
486
|
+
assert.equal(page3.nextOffset, undefined, `越界页无 nextOffset,实际 ${page3.nextOffset}`);
|
|
487
|
+
assert.equal(page3.truncated, false, `越界页 truncated 应 false`);
|
|
488
|
+
assert.equal(page3.offset, 60, `越界页 offset echo 应回填请求值 60`);
|
|
489
|
+
// 第 1 页首条应是 mtime 最新(序号最大)
|
|
490
|
+
assert.ok(page1.artifacts[0]?.path.endsWith('itinerary-059.md'), `首条应是 mtime 最新者,实际 ${page1.artifacts[0]?.path}`);
|
|
491
|
+
// 第 3 页(越界页)首条之后无内容,但 known total 仍准确
|
|
492
|
+
console.log('19) >50 cwd 跨页枚举 OK');
|
|
493
|
+
}
|
|
494
|
+
finally {
|
|
495
|
+
rmSync(bigCwd, { recursive: true, force: true });
|
|
496
|
+
rmSync(bigRoot, { recursive: true, force: true });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// 20) equal timestamps:lexical (source,id,path) tie-break 让 page 边界稳定
|
|
500
|
+
{
|
|
501
|
+
const tieRoot = mkdtempSync(join(tmpdir(), 'gotry-458-tie-root-'));
|
|
502
|
+
const sameCwd = mkdtempSync(join(tmpdir(), 'gotry-458-tie-'));
|
|
503
|
+
try {
|
|
504
|
+
const ts = new Date('2026-01-01T00:00:00.000Z');
|
|
505
|
+
const { utimesSync } = await import('node:fs');
|
|
506
|
+
for (const name of ['zeta.md', 'alpha.md', 'Beta.md', 'Gamma.md']) {
|
|
507
|
+
const p = join(sameCwd, name);
|
|
508
|
+
writeFileSync(p, '# tie\n');
|
|
509
|
+
utimesSync(p, ts, ts);
|
|
510
|
+
}
|
|
511
|
+
const r = await listArtifacts({ stateRoot: tieRoot, cwd: sameCwd, limit: 2 });
|
|
512
|
+
assert.equal(r.total, 4, `total 应 4,实际 ${r.total}`);
|
|
513
|
+
assert.equal(r.artifacts.length, 2);
|
|
514
|
+
// 标题去扩展名后 codepoint 词典序(ASCII):'B'(0x42) < 'G'(0x47) < 'a'(0x61) < 'z'(0x7A)
|
|
515
|
+
const titles = r.artifacts.map(a => a.title);
|
|
516
|
+
assert.deepEqual(titles, ['Beta', 'Gamma'], `updated 相同时按 title codepoint 词典序,实际 ${titles.join(',')}`);
|
|
517
|
+
// 同样输入两次必须返回完全相同的顺序
|
|
518
|
+
const r2 = await listArtifacts({ stateRoot: tieRoot, cwd: sameCwd, limit: 2 });
|
|
519
|
+
assert.deepEqual(r2.artifacts.map(a => a.path), r.artifacts.map(a => a.path), '等 mtime 下分页顺序必须可复现');
|
|
520
|
+
console.log('20) equal-timestamp tie-break OK');
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
rmSync(sameCwd, { recursive: true, force: true });
|
|
524
|
+
rmSync(tieRoot, { recursive: true, force: true });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// 21) mixed ledger/legacy/cwd:authority + dedupe 跨分页保留
|
|
528
|
+
{
|
|
529
|
+
const mixRoot = mkdtempSync(join(tmpdir(), 'gotry-458-mix-root-'));
|
|
530
|
+
const mixCwd = mkdtempSync(join(tmpdir(), 'gotry-458-mix-cwd-'));
|
|
531
|
+
try {
|
|
532
|
+
const ledger = ensureLedger(mixRoot);
|
|
533
|
+
ledger.createWorkflowRun({ id: 'cap-mix-a', goal: '账本权威 a', ticket: { objective: 'mix' }, state: {} });
|
|
534
|
+
ledger.settleWorkflowRun('cap-mix-a', '# a\n');
|
|
535
|
+
ledger.createWorkflowRun({ id: 'cap-mix-b', goal: '账本权威 b', ticket: { objective: 'mix' }, state: {} });
|
|
536
|
+
ledger.settleWorkflowRun('cap-mix-b', '# b\n');
|
|
537
|
+
mkdirSync(join(mixRoot, 'gotry-state', 'async'), { recursive: true });
|
|
538
|
+
writeFileSync(join(mixRoot, 'gotry-state', 'async', 'legacy-1.deliverable.md'), '# legacy\n');
|
|
539
|
+
writeFileSync(join(mixRoot, 'gotry-state', 'async', 'legacy-2.deliverable.md'), '# legacy\n');
|
|
540
|
+
writeFileSync(join(mixCwd, 'trip-plan-a.md'), '# cwd a\n');
|
|
541
|
+
writeFileSync(join(mixCwd, 'trip-plan-b.md'), '# cwd b\n');
|
|
542
|
+
const r = await listArtifacts({ stateRoot: mixRoot, cwd: mixCwd, limit: 10 });
|
|
543
|
+
assert.equal(r.total, 6, `total 应 6(2 账本 + 2 legacy + 2 cwd),实际 ${r.total}`);
|
|
544
|
+
const sources = new Set(r.artifacts.map(a => a.source));
|
|
545
|
+
assert.ok(sources.has('async-run') && sources.has('cwd-file'), `应同时覆盖账本/legacy 与 cwd 两源,实际 ${[...sources].join(',')}`);
|
|
546
|
+
// 写入与账本同 path 的文件视图,验证 dedupe 后总数不变
|
|
547
|
+
writeFileSync(join(mixRoot, 'gotry-state', 'async', 'cap-mix-a.deliverable.md'), '# file view a\n');
|
|
548
|
+
writeFileSync(join(mixRoot, 'gotry-state', 'async', 'cap-mix-b.deliverable.md'), '# file view b\n');
|
|
549
|
+
const r2 = await listArtifacts({ stateRoot: mixRoot, cwd: mixCwd, limit: 10 });
|
|
550
|
+
assert.equal(r2.total, 6, `账本权威 + 文件视图 dedupe 后 total 应仍 6,实际 ${r2.total}`);
|
|
551
|
+
const seen = new Map();
|
|
552
|
+
for (const a of r2.artifacts) {
|
|
553
|
+
seen.set(resolve(a.path), (seen.get(resolve(a.path)) ?? 0) + 1);
|
|
554
|
+
}
|
|
555
|
+
for (const [k, n] of seen) {
|
|
556
|
+
assert.ok(n <= 1, `path ${k} 出现 ${n} 次(账本/legacy/cwd 跨源去重失败)`);
|
|
557
|
+
}
|
|
558
|
+
// 再次确认源覆盖:每个 cap-mix-{a,b} 应来自账本(async-run),不是 cwd
|
|
559
|
+
for (const id of ['cap-mix-a', 'cap-mix-b']) {
|
|
560
|
+
const entry = r2.artifacts.find(a => a.id === id);
|
|
561
|
+
assert.equal(entry?.source, 'async-run', `${id} 应来自账本权威,实际 ${entry?.source}`);
|
|
562
|
+
}
|
|
563
|
+
console.log('21) mixed ledger/legacy/cwd dedupe OK');
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
rmSync(mixRoot, { recursive: true, force: true });
|
|
567
|
+
rmSync(mixCwd, { recursive: true, force: true });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// 22) literal search:finds old entries before paging;total reflects filtered set
|
|
571
|
+
{
|
|
572
|
+
const searchRoot = mkdtempSync(join(tmpdir(), 'gotry-458-search-root-'));
|
|
573
|
+
const searchCwd = mkdtempSync(join(tmpdir(), 'gotry-458-search-'));
|
|
574
|
+
try {
|
|
575
|
+
for (const name of ['trip-paris-2025.md', 'trip-tokyo-2024.md', 'report-budget.md', 'random.md']) {
|
|
576
|
+
writeFileSync(join(searchCwd, name), `# ${name}\n`);
|
|
577
|
+
}
|
|
578
|
+
const exact = await listArtifacts({ stateRoot: searchRoot, cwd: searchCwd, search: 'paris' });
|
|
579
|
+
assert.equal(exact.total, 1, `search='paris' 应命中 1 条,实际 ${exact.total}`);
|
|
580
|
+
assert.equal(exact.artifacts[0]?.id, 'trip-paris-2025.md');
|
|
581
|
+
const none = await listArtifacts({ stateRoot: searchRoot, cwd: searchCwd, search: ' ' });
|
|
582
|
+
assert.equal(none.total, 4, `空白 search 应等同未过滤,total=4,实际 ${none.total}`);
|
|
583
|
+
const ci = await listArtifacts({ stateRoot: searchRoot, cwd: searchCwd, search: 'PARIS' });
|
|
584
|
+
assert.equal(ci.total, 1, `search 大小写不敏感,实际 ${ci.total}`);
|
|
585
|
+
const literal = await listArtifacts({ stateRoot: searchRoot, cwd: searchCwd, search: '.*' });
|
|
586
|
+
assert.equal(literal.total, 0, `.* 应作字面匹配,不命中任何 id/title/filename,实际 ${literal.total}`);
|
|
587
|
+
const bodyOnly = await listArtifacts({ stateRoot: searchRoot, cwd: searchCwd, search: 'budget-detail' });
|
|
588
|
+
assert.equal(bodyOnly.total, 0, `search 仅匹配元数据,不搜文件内容,实际 ${bodyOnly.total}`);
|
|
589
|
+
// search 后分页 + 越界
|
|
590
|
+
const filtered = await listArtifacts({ stateRoot: searchRoot, cwd: searchCwd, search: 'trip', limit: 2 });
|
|
591
|
+
assert.equal(filtered.total, 2, `search='trip' 应命中 trip-* 两条,实际 ${filtered.total}`);
|
|
592
|
+
assert.equal(filtered.nextOffset, undefined, `2 条全在一页时无 nextOffset`);
|
|
593
|
+
// 元数据命中 via title(去扩展名)
|
|
594
|
+
const viaTitle = await listArtifacts({ stateRoot: searchRoot, cwd: searchCwd, search: 'report-budget' });
|
|
595
|
+
assert.equal(viaTitle.total, 1, `命中 id,实际 ${viaTitle.total}`);
|
|
596
|
+
// 标记 search echo
|
|
597
|
+
assert.equal(filtered.search, 'trip', `应回填 trim 后的 search,实际 ${filtered.search}`);
|
|
598
|
+
console.log('22) literal search OK');
|
|
599
|
+
}
|
|
600
|
+
finally {
|
|
601
|
+
rmSync(searchCwd, { recursive: true, force: true });
|
|
602
|
+
rmSync(searchRoot, { recursive: true, force: true });
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// 23) invalid offset/limit/search types:拒绝可预测,不静默圆整
|
|
606
|
+
{
|
|
607
|
+
await assert.rejects(() => listArtifacts({ stateRoot, cwd, offset: -1 }), /offset must be a nonnegative integer/, '负 offset 必须被拒');
|
|
608
|
+
await assert.rejects(() => listArtifacts({ stateRoot, cwd, offset: 1.5 }), /offset must be a nonnegative integer/, '非整数 offset 必须被拒');
|
|
609
|
+
await assert.rejects(() => listArtifacts({ stateRoot, cwd, offset: NaN }), /offset must be a nonnegative integer/, 'NaN offset 必须被拒');
|
|
610
|
+
await assert.rejects(() => listArtifacts({ stateRoot, cwd, offset: '5' }), /offset must be a nonnegative integer/, '字符串 offset 必须被拒(不静默转数字)');
|
|
611
|
+
await assert.rejects(() => listArtifacts({ stateRoot, cwd, offset: null }), /offset must be a nonnegative integer/, 'null offset 必须被拒');
|
|
612
|
+
await assert.rejects(() => listArtifacts({ stateRoot, cwd, offset: Number.MAX_SAFE_INTEGER + 1 }), /offset must be a safe integer/, 'unsafe 整数 offset(>MAX_SAFE_INTEGER)必须被拒,避免数组切片失真');
|
|
613
|
+
await assert.rejects(() => listArtifacts({ stateRoot, cwd, offset: -Number.MAX_SAFE_INTEGER - 1 }), /offset must be/, 'unsafe 负整数 offset(<-MAX_SAFE_INTEGER)必须被拒(由负数分支先拒)');
|
|
614
|
+
await assert.rejects(() => listArtifacts({ stateRoot, cwd, limit: '20' }), /limit must be a positive integer/, '字符串 limit 必须被拒');
|
|
615
|
+
await assert.rejects(() => listArtifacts({ stateRoot, cwd, limit: Infinity }), /limit must be a positive integer/, 'Infinity limit 必须被拒');
|
|
616
|
+
await assert.rejects(() => listArtifacts({ stateRoot, cwd, limit: 1.5 }), /limit must be a positive integer/, '非整数 limit 必须被拒');
|
|
617
|
+
await assert.rejects(() => listArtifacts({ stateRoot, cwd, search: 5 }), /search must be a string/, '非字符串 search 必须被拒(不静默 "no filter")');
|
|
618
|
+
// 兼容路径:零/负 INTEGER limit 静默夹到 1(保留 pre-#458 clamp-on-1 行为)
|
|
619
|
+
const zero = await listArtifacts({ stateRoot, cwd, limit: 0 });
|
|
620
|
+
assert.equal(zero.limit, 1, `limit=0 应夹到 1,实际 ${zero.limit}`);
|
|
621
|
+
assert.equal(zero.artifacts.length <= 1, true, `limit 夹到 1 时最多 1 条`);
|
|
622
|
+
const neg = await listArtifacts({ stateRoot, cwd, limit: -3 });
|
|
623
|
+
assert.equal(neg.limit, 1, `limit=-3 应夹到 1,实际 ${neg.limit}`);
|
|
624
|
+
// 大值夹到 50
|
|
625
|
+
const big = await listArtifacts({ stateRoot, cwd, limit: 9999 });
|
|
626
|
+
assert.equal(big.limit, 50, `limit=9999 应夹到 50,实际 ${big.limit}`);
|
|
627
|
+
// 友好情况:undefined 走默认
|
|
628
|
+
const ok = await listArtifacts({ stateRoot, cwd });
|
|
629
|
+
assert.equal(ok.offset, 0, `undefined offset 应回填 0`);
|
|
630
|
+
assert.equal(ok.limit, 20, `undefined limit 应回填默认 20`);
|
|
631
|
+
console.log('23) invalid offset/limit/search OK');
|
|
632
|
+
}
|
|
633
|
+
// 24) search + paging:在 >50 条里用 search 找到"旧"的精确产物
|
|
634
|
+
{
|
|
635
|
+
const bigRoot = mkdtempSync(join(tmpdir(), 'gotry-458-bigsearch-root-'));
|
|
636
|
+
const bigCwd = mkdtempSync(join(tmpdir(), 'gotry-458-bigsearch-'));
|
|
637
|
+
try {
|
|
638
|
+
const { utimesSync } = await import('node:fs');
|
|
639
|
+
for (let i = 0; i < 60; i++) {
|
|
640
|
+
const name = `note-${String(i).padStart(3, '0')}.md`;
|
|
641
|
+
const p = join(bigCwd, name);
|
|
642
|
+
writeFileSync(p, `# ${name}\n`);
|
|
643
|
+
utimesSync(p, new Date(Date.UTC(2026, 0, 1, 0, 0, i)), new Date(Date.UTC(2026, 0, 1, 0, 0, i)));
|
|
644
|
+
}
|
|
645
|
+
// 极小 mtime 的目标条目(模拟"老产物")
|
|
646
|
+
const targetPath = join(bigCwd, 'special-deliverable.md');
|
|
647
|
+
writeFileSync(targetPath, '# special\n');
|
|
648
|
+
utimesSync(targetPath, new Date('2020-01-01T00:00:00Z'), new Date('2020-01-01T00:00:00Z'));
|
|
649
|
+
// 不带 search:即便用 limit=50 也找不到极老的产物(默认按 mtime DESC 排序)
|
|
650
|
+
const noSearch = await listArtifacts({ stateRoot: bigRoot, cwd: bigCwd, limit: 50 });
|
|
651
|
+
assert.equal(noSearch.total, 61, `61 条总数,实际 ${noSearch.total}`);
|
|
652
|
+
assert.ok(!noSearch.artifacts.some(a => a.id === 'special-deliverable.md'), '无 search 时老产物应在 50 之外');
|
|
653
|
+
assert.equal(noSearch.truncated, true, '60 条之外还有老产物时 truncated 应 true');
|
|
654
|
+
// 带 search:即使 limit=1 也能直接定位
|
|
655
|
+
const direct = await listArtifacts({ stateRoot: bigRoot, cwd: bigCwd, search: 'special-deliverable', limit: 1 });
|
|
656
|
+
assert.equal(direct.total, 1, `search 命中 1,实际 ${direct.total}`);
|
|
657
|
+
assert.equal(direct.artifacts[0]?.id, 'special-deliverable.md');
|
|
658
|
+
assert.equal(direct.truncated, false, `1 条命中无下一页`);
|
|
659
|
+
// search + 翻页:filter 后 total 反映 filtered 集
|
|
660
|
+
const pageFilter = await listArtifacts({ stateRoot: bigRoot, cwd: bigCwd, search: 'note', limit: 20 });
|
|
661
|
+
assert.equal(pageFilter.total, 60, `search='note' 应命中 60 条 note-*,实际 ${pageFilter.total}`);
|
|
662
|
+
assert.equal(pageFilter.artifacts.length, 20);
|
|
663
|
+
assert.equal(pageFilter.nextOffset, 20, `filter 后 60 条 / limit 20 → nextOffset 20`);
|
|
664
|
+
const page2 = await listArtifacts({ stateRoot, cwd: bigCwd, search: 'note', limit: 20, offset: 20 });
|
|
665
|
+
assert.equal(page2.total, 60);
|
|
666
|
+
assert.equal(page2.artifacts.length, 20);
|
|
667
|
+
assert.equal(page2.nextOffset, 40, `page2 nextOffset 应 40`);
|
|
668
|
+
// 两页拼起来应不重复
|
|
669
|
+
const seen = new Set();
|
|
670
|
+
for (const a of [...pageFilter.artifacts, ...page2.artifacts])
|
|
671
|
+
seen.add(a.path);
|
|
672
|
+
assert.equal(seen.size, 40, `两页拼起来 40 个唯一 path,实际 ${seen.size}`);
|
|
673
|
+
console.log('24) search + paging across >50 OK');
|
|
674
|
+
}
|
|
675
|
+
finally {
|
|
676
|
+
rmSync(bigCwd, { recursive: true, force: true });
|
|
677
|
+
rmSync(bigRoot, { recursive: true, force: true });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// 25) existing read-by-id/path 边界在新分页表面下不回归
|
|
681
|
+
{
|
|
682
|
+
// a) 已存在的裸 id 直接读
|
|
683
|
+
const r = await readArtifact({ stateRoot, cwd, path: 'cap-probe-1' });
|
|
684
|
+
assert.equal(r.ok, true, '裸工单 id 仍可读');
|
|
685
|
+
if (r.ok)
|
|
686
|
+
assert.equal(r.lang, 'markdown');
|
|
687
|
+
// b) 分页结果里某条 path 仍可直接用 readArtifact 读回 — 必须用已知小
|
|
688
|
+
// fixture(trip-2027-cap.md),不能选首条 cwd 项:先前测试(10/17a/24)可能
|
|
689
|
+
// 在 cwd 留下 big.md(>2MB)被 reader 拒,需要隔离 / 显式选择。
|
|
690
|
+
writeFileSync(join(cwd, 'paging-target.md'), '# paging target\nbody\n');
|
|
691
|
+
const list = await listArtifacts({ stateRoot, cwd, limit: 50 });
|
|
692
|
+
const target = list.artifacts.find(a => a.id === 'paging-target.md');
|
|
693
|
+
assert.ok(target, '列表里应有 paging-target.md 已知小 fixture');
|
|
694
|
+
const read = await readArtifact({ stateRoot, cwd, path: target.path });
|
|
695
|
+
assert.equal(read.ok, true, 'list 返回的 path 应可读回');
|
|
696
|
+
if (read.ok)
|
|
697
|
+
assert.equal(read.lang, 'markdown');
|
|
698
|
+
rmSync(join(cwd, 'paging-target.md'));
|
|
699
|
+
// c) 越界路径仍被拒
|
|
700
|
+
const out = await readArtifact({ stateRoot, cwd, path: '../../../../../etc/passwd' });
|
|
701
|
+
assert.equal(out.ok, false, '越界仍必须被拒');
|
|
702
|
+
// d) symlink 越界仍被拒
|
|
703
|
+
const linkPath = join(cwd, 'symlink-page-out.md');
|
|
704
|
+
try {
|
|
705
|
+
symlinkSync('/etc/passwd', linkPath);
|
|
706
|
+
const r2 = await readArtifact({ stateRoot, cwd, path: 'symlink-page-out.md' });
|
|
707
|
+
assert.equal(r2.ok, false, 'symlink 逃逸仍被拒');
|
|
708
|
+
}
|
|
709
|
+
catch { /* 非 POSIX fs 跳过 */ }
|
|
710
|
+
// e) node_modules 内既不可发现也不可读
|
|
711
|
+
mkdirSync(join(cwd, 'node_modules'), { recursive: true });
|
|
712
|
+
writeFileSync(join(cwd, 'node_modules', 'page-evil.md'), '# evil');
|
|
713
|
+
const denied = await readArtifact({ stateRoot, cwd, path: 'node_modules/page-evil.md' });
|
|
714
|
+
assert.equal(denied.ok, false, 'node_modules 内文件仍被拒');
|
|
715
|
+
const list2 = await listArtifacts({ stateRoot, cwd, limit: 50 });
|
|
716
|
+
assert.ok(!list2.artifacts.some(a => a.id === 'page-evil.md'), 'node_modules 内文件不进入列表');
|
|
717
|
+
rmSync(join(cwd, 'node_modules'), { recursive: true, force: true });
|
|
718
|
+
// f) 不支持的扩展名仍被拒
|
|
719
|
+
writeFileSync(join(cwd, 'binary.db'), 'fake');
|
|
720
|
+
const bad = await readArtifact({ stateRoot, cwd, path: 'binary.db' });
|
|
721
|
+
assert.equal(bad.ok, false, '.db 仍被拒');
|
|
722
|
+
rmSync(join(cwd, 'binary.db'));
|
|
723
|
+
console.log('25) read boundary 与分页表面兼容 OK');
|
|
724
|
+
}
|
|
725
|
+
rmSync(stateRoot, { recursive: true, force: true });
|
|
726
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
727
|
+
console.log('all artifacts capability tests OK');
|