@zhushanwen/pi-subagent-workflow 8.5.0 → 8.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -16
- package/relay/relay.mjs +1 -1
- package/src/host/__tests__/pi-host.test.ts +214 -0
- package/src/host/pi-host.ts +109 -0
- package/src/index.ts +142 -32
- package/src/injectors/__tests__/engine-awareness.test.ts +292 -0
- package/src/injectors/__tests__/engine-section-stability.test.ts +341 -0
- package/src/injectors/__tests__/subagent-list-injector.test.ts +5 -5
- package/src/injectors/__tests__/workflow-list-injector.test.ts +5 -5
- package/src/injectors/engine-awareness.ts +123 -0
- package/src/injectors/model-list-injector.ts +1 -1
- package/src/injectors/subagent-list-injector.ts +8 -8
- package/src/injectors/workflow-list-injector.ts +8 -8
- package/src/interface/__tests__/detectors.test.ts +3 -3
- package/src/interface/__tests__/subagent-tool-path-guard.test.ts +157 -0
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +12 -0
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +2 -2
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +1 -1
- package/src/interface/bg-notify-render.ts +34 -13
- package/src/interface/commands.ts +3 -3
- package/src/interface/format.ts +2 -2
- package/src/interface/helpers.ts +3 -3
- package/src/interface/list-component.ts +4 -4
- package/src/interface/list-shared.ts +2 -2
- package/src/interface/list-view.ts +2 -2
- package/src/interface/subagent-actions.ts +288 -19
- package/src/interface/subagent-tool-schema.ts +163 -0
- package/src/interface/subagent-tool.ts +65 -129
- package/src/interface/subagents.ts +5 -5
- package/src/interface/tool-render.ts +34 -2
- package/src/interface/tool-workflow-script.ts +4 -4
- package/src/interface/tool-workflow.ts +6 -6
- package/src/interface/views/WorkflowsView.ts +5 -5
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +2 -2
- package/src/interface/views/detail-content.ts +4 -4
- package/src/interface/views/format.ts +4 -4
- package/src/{orchestration/jsonl-run-store.ts → jsonl-run-store.ts} +132 -18
- package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +0 -53
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +0 -1
- package/src/execution/__tests__/agent-registry.test.ts +0 -363
- package/src/execution/__tests__/agent-result-mapper.test.ts +0 -150
- package/src/execution/__tests__/alive-store.test.ts +0 -147
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
- package/src/execution/__tests__/before-agent-start-injection.test.ts +0 -132
- package/src/execution/__tests__/bg-notify-render.test.ts +0 -256
- package/src/execution/__tests__/channel-registry-handshake.test.ts +0 -243
- package/src/execution/__tests__/chat-engine-routing.test.ts +0 -597
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +0 -365
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -190
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +0 -215
- package/src/execution/__tests__/concurrency-pool.test.ts +0 -250
- package/src/execution/__tests__/config.test.ts +0 -110
- package/src/execution/__tests__/conversation-wiring.test.ts +0 -198
- package/src/execution/__tests__/crash-recovery.test.ts +0 -320
- package/src/execution/__tests__/delivery-methods.test.ts +0 -385
- package/src/execution/__tests__/dialog-queue.test.ts +0 -299
- package/src/execution/__tests__/epipe-fallback.test.ts +0 -241
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -243
- package/src/execution/__tests__/execute-nesting.test.ts +0 -323
- package/src/execution/__tests__/execute-options-mapper.test.ts +0 -189
- package/src/execution/__tests__/execution-record.test.ts +0 -1284
- package/src/execution/__tests__/finalize-record.test.ts +0 -368
- package/src/execution/__tests__/finalized-marker.test.ts +0 -82
- package/src/execution/__tests__/format-schema-instruction.test.ts +0 -138
- package/src/execution/__tests__/format.test.ts +0 -458
- package/src/execution/__tests__/gc-timer.test.ts +0 -184
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +0 -254
- package/src/execution/__tests__/gui-mode-dispatch.test.ts +0 -59
- package/src/execution/__tests__/helpers/mock-extension-api.ts +0 -30
- package/src/execution/__tests__/helpers/spawn-mock.ts +0 -238
- package/src/execution/__tests__/host-mode.test.ts +0 -87
- package/src/execution/__tests__/index-session-start-identity.test.ts +0 -371
- package/src/execution/__tests__/index-session-start.test.ts +0 -593
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
- package/src/execution/__tests__/lifecycle-manager.test.ts +0 -337
- package/src/execution/__tests__/lifecycle-predicates.test.ts +0 -116
- package/src/execution/__tests__/list-component.test.ts +0 -401
- package/src/execution/__tests__/list-fields.test.ts +0 -109
- package/src/execution/__tests__/manifest-parentid.test.ts +0 -117
- package/src/execution/__tests__/model-resolver.test.ts +0 -413
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +0 -287
- package/src/execution/__tests__/nested-visibility.test.ts +0 -325
- package/src/execution/__tests__/notifier-flush.test.ts +0 -574
- package/src/execution/__tests__/notifier-golden-snapshot.test.ts +0 -261
- package/src/execution/__tests__/one-shot-upgrade.test.ts +0 -205
- package/src/execution/__tests__/output-collector.test.ts +0 -61
- package/src/execution/__tests__/parent-child-matrix.test.ts +0 -336
- package/src/execution/__tests__/path-encoding.test.ts +0 -104
- package/src/execution/__tests__/pi-invocation.test.ts +0 -134
- package/src/execution/__tests__/record-store.test.ts +0 -1057
- package/src/execution/__tests__/records-cwd-isolation.test.ts +0 -91
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +0 -339
- package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -273
- package/src/execution/__tests__/relay-agent.test.ts +0 -448
- package/src/execution/__tests__/relay-env.test.ts +0 -42
- package/src/execution/__tests__/resource-policy.test.ts +0 -109
- package/src/execution/__tests__/rpc-mode.test.ts +0 -89
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -267
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -253
- package/src/execution/__tests__/run-spawn-edges.test.ts +0 -644
- package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
- package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
- package/src/execution/__tests__/sdk-contract.test.ts +0 -273
- package/src/execution/__tests__/session-context-resolver.test.ts +0 -167
- package/src/execution/__tests__/session-file-gc.test.ts +0 -293
- package/src/execution/__tests__/session-pending.test.ts +0 -197
- package/src/execution/__tests__/session-reconstructor.test.ts +0 -379
- package/src/execution/__tests__/session-runner-epipe.test.ts +0 -178
- package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -311
- package/src/execution/__tests__/session-start-reaper.test.ts +0 -233
- package/src/execution/__tests__/spawn-args.test.ts +0 -434
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -189
- package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -207
- package/src/execution/__tests__/spawned-children.test.ts +0 -92
- package/src/execution/__tests__/startup-config-declaration.test.ts +0 -35
- package/src/execution/__tests__/status-refactor.test.ts +0 -345
- package/src/execution/__tests__/stdin-writer.test.ts +0 -463
- package/src/execution/__tests__/stream-sink-retirement.test.ts +0 -261
- package/src/execution/__tests__/subagent-service-abort.test.ts +0 -60
- package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -629
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +0 -180
- package/src/execution/__tests__/subagent-service.test.ts +0 -788
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -310
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +0 -506
- package/src/execution/__tests__/temp-prompt.test.ts +0 -53
- package/src/execution/__tests__/timeout-integration.test.ts +0 -397
- package/src/execution/__tests__/tombstone-store.test.ts +0 -73
- package/src/execution/__tests__/tool-action.test.ts +0 -385
- package/src/execution/__tests__/truncline-snapshot.test.ts +0 -81
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
- package/src/execution/__tests__/turn-limiter.test.ts +0 -65
- package/src/execution/__tests__/ui-channels.test.ts +0 -187
- package/src/execution/__tests__/ui-interaction-model.test.ts +0 -67
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +0 -370
- package/src/execution/__tests__/ui-request-handler.test.ts +0 -204
- package/src/execution/__tests__/ui-request-observability.test.ts +0 -114
- package/src/execution/__tests__/ui-request-queue.test.ts +0 -133
- package/src/execution/__tests__/worktree-manager.test.ts +0 -633
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -229
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +0 -181
- package/src/execution/__tests__/worktree-registry.test.ts +0 -199
- package/src/execution/agent-registry.ts +0 -193
- package/src/execution/agent-result-mapper.ts +0 -90
- package/src/execution/alive-store.ts +0 -92
- package/src/execution/argv-mirror.ts +0 -109
- package/src/execution/best-effort.ts +0 -38
- package/src/execution/channel-registry-access.ts +0 -144
- package/src/execution/concurrency-pool.ts +0 -116
- package/src/execution/config.ts +0 -96
- package/src/execution/dialog-queue.ts +0 -330
- package/src/execution/engine/__tests__/common/data-dir.test.ts +0 -53
- package/src/execution/engine/__tests__/common/errors.test.ts +0 -132
- package/src/execution/engine/__tests__/common/event-journal.test.ts +0 -177
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +0 -192
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +0 -81
- package/src/execution/engine/__tests__/common/persona-router.test.ts +0 -123
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +0 -154
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
- package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +0 -141
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +0 -109
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +0 -101
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +0 -77
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +0 -104
- package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +0 -342
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +0 -201
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +0 -76
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +0 -79
- package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -87
- package/src/execution/engine/__tests__/engines-declaration.test.ts +0 -36
- package/src/execution/engine/__tests__/model-prompt.test.ts +0 -85
- package/src/execution/engine/__tests__/paths.test.ts +0 -39
- package/src/execution/engine/__tests__/registry.test.ts +0 -120
- package/src/execution/engine/__tests__/routing.test.ts +0 -231
- package/src/execution/engine/common/data-dir.ts +0 -62
- package/src/execution/engine/common/errors.ts +0 -183
- package/src/execution/engine/common/event-journal.ts +0 -254
- package/src/execution/engine/common/journal-replay.ts +0 -62
- package/src/execution/engine/common/kill-chain.ts +0 -221
- package/src/execution/engine/common/nesting-guard.ts +0 -50
- package/src/execution/engine/common/persona-router.ts +0 -108
- package/src/execution/engine/common/pool-manager.ts +0 -226
- package/src/execution/engine/common/schema-emulation.ts +0 -189
- package/src/execution/engine/common/session-view-projection.ts +0 -51
- package/src/execution/engine/engine-discovery.ts +0 -65
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -469
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
- package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
- package/src/execution/engine/engines/pi/pi-engine.ts +0 -415
- package/src/execution/engine/engines/pi/reader.ts +0 -48
- package/src/execution/engine/engines/pi/registration.ts +0 -35
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
- package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -246
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -228
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -64
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -127
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -567
- package/src/execution/engine/engines/zcode/constants.ts +0 -43
- package/src/execution/engine/engines/zcode/golden-sample.ts +0 -39
- package/src/execution/engine/engines/zcode/launcher.ts +0 -161
- package/src/execution/engine/engines/zcode/parser.ts +0 -436
- package/src/execution/engine/engines/zcode/preparer.ts +0 -363
- package/src/execution/engine/engines/zcode/reader.ts +0 -381
- package/src/execution/engine/engines/zcode/registration.ts +0 -37
- package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -648
- package/src/execution/engine/host-task-spec.ts +0 -47
- package/src/execution/engine/model-prompt.ts +0 -59
- package/src/execution/engine/paths.ts +0 -42
- package/src/execution/engine/port.ts +0 -153
- package/src/execution/engine/registry.ts +0 -123
- package/src/execution/engine/routing.ts +0 -218
- package/src/execution/engine/types.ts +0 -304
- package/src/execution/execute-options-mapper.ts +0 -111
- package/src/execution/execution-record.ts +0 -906
- package/src/execution/finalize-record.ts +0 -251
- package/src/execution/finalized-marker.ts +0 -51
- package/src/execution/get-state-handshake.ts +0 -104
- package/src/execution/host-mode.ts +0 -56
- package/src/execution/idle-gc.ts +0 -47
- package/src/execution/lifecycle-manager.ts +0 -491
- package/src/execution/lifecycle-predicates.ts +0 -65
- package/src/execution/manifest-store.ts +0 -256
- package/src/execution/model-config-service.ts +0 -204
- package/src/execution/model-resolver.ts +0 -276
- package/src/execution/notifier.ts +0 -260
- package/src/execution/output-collector.ts +0 -88
- package/src/execution/path-encoding.ts +0 -60
- package/src/execution/pi-invocation.ts +0 -120
- package/src/execution/record-entry.ts +0 -132
- package/src/execution/record-store.ts +0 -1234
- package/src/execution/relay-env.ts +0 -37
- package/src/execution/session-context-resolver.ts +0 -64
- package/src/execution/session-file-gc.ts +0 -120
- package/src/execution/session-pending.ts +0 -164
- package/src/execution/session-reconstructor.ts +0 -678
- package/src/execution/session-runner.ts +0 -1548
- package/src/execution/sessions-index.ts +0 -304
- package/src/execution/spawn-event-adapter.ts +0 -363
- package/src/execution/stdin-writer.ts +0 -198
- package/src/execution/stream-sink.ts +0 -126
- package/src/execution/subagent-service.ts +0 -2119
- package/src/execution/subprocess-agent-runner.ts +0 -303
- package/src/execution/temp-prompt.ts +0 -62
- package/src/execution/tombstone-store.ts +0 -72
- package/src/execution/turn-limiter.ts +0 -102
- package/src/execution/types.ts +0 -910
- package/src/execution/ui-channels.ts +0 -216
- package/src/execution/ui-interaction-model.ts +0 -48
- package/src/execution/ui-request-handler-factory.ts +0 -212
- package/src/execution/ui-request-observability.ts +0 -81
- package/src/execution/ui-request-queue.ts +0 -174
- package/src/execution/worktree-manager.ts +0 -688
- package/src/execution/worktree-registry.ts +0 -222
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +0 -336
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -208
- package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -157
- package/src/orchestration/__tests__/args-validator.test.ts +0 -143
- package/src/orchestration/__tests__/concurrency-gate.test.ts +0 -125
- package/src/orchestration/__tests__/config-loader.test.ts +0 -503
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +0 -724
- package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +0 -405
- package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +0 -56
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +0 -166
- package/src/orchestration/__tests__/execute-agent-call.test.ts +0 -314
- package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +0 -171
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +0 -1030
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -277
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +0 -96
- package/src/orchestration/__tests__/lifecycle.test.ts +0 -625
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +0 -1971
- package/src/orchestration/__tests__/script-lint.test.ts +0 -831
- package/src/orchestration/__tests__/skill-discovery.test.ts +0 -157
- package/src/orchestration/__tests__/test-mocks.ts +0 -197
- package/src/orchestration/__tests__/worker-host.test.ts +0 -120
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +0 -164
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +0 -520
- package/src/orchestration/__tests__/worker-script-builder.test.ts +0 -309
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +0 -110
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -317
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +0 -110
- package/src/orchestration/__tests__/workflows-e2e.test.ts +0 -537
- package/src/orchestration/agent-opts-resolver.ts +0 -93
- package/src/orchestration/args-validator.ts +0 -127
- package/src/orchestration/concurrency-gate.ts +0 -69
- package/src/orchestration/config-loader.ts +0 -315
- package/src/orchestration/error-recovery.ts +0 -862
- package/src/orchestration/execute-agent-call.ts +0 -217
- package/src/orchestration/launcher.ts +0 -410
- package/src/orchestration/lifecycle.ts +0 -396
- package/src/orchestration/models/__tests__/budget.test.ts +0 -367
- package/src/orchestration/models/__tests__/trace.test.ts +0 -408
- package/src/orchestration/models/agent-call.ts +0 -83
- package/src/orchestration/models/budget.ts +0 -148
- package/src/orchestration/models/ports.ts +0 -164
- package/src/orchestration/models/run-runtime.ts +0 -89
- package/src/orchestration/models/run-spec.ts +0 -83
- package/src/orchestration/models/run-state.ts +0 -44
- package/src/orchestration/models/trace.ts +0 -183
- package/src/orchestration/models/types.ts +0 -292
- package/src/orchestration/models/workflow-run.ts +0 -254
- package/src/orchestration/models/workflow-script-registry.ts +0 -35
- package/src/orchestration/models/workflow-script.ts +0 -117
- package/src/orchestration/script-lint.ts +0 -766
- package/src/orchestration/skill-discovery.ts +0 -92
- package/src/orchestration/worker-handle.ts +0 -115
- package/src/orchestration/worker-host.ts +0 -98
- package/src/orchestration/worker-script-builder.ts +0 -408
- package/src/orchestration/workflow-files.ts +0 -85
- package/src/orchestration/workflow-script-registry-impl.ts +0 -133
- package/src/shared/__tests__/agent-ref.test.ts +0 -34
- package/src/shared/__tests__/meta-parser.test.ts +0 -304
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -280
- package/src/shared/__tests__/resource-discovery.test.ts +0 -736
- package/src/shared/__tests__/resource-meta.test.ts +0 -51
- package/src/shared/__tests__/schema-jsonify.test.ts +0 -81
- package/src/shared/agent-event.ts +0 -13
- package/src/shared/agent-ref.ts +0 -57
- package/src/shared/meta-parser.ts +0 -261
- package/src/shared/resource-discovery.ts +0 -811
- package/src/shared/resource-meta.ts +0 -65
- package/src/shared/schema-jsonify.ts +0 -56
- package/src/shared/xml-injection.ts +0 -35
- package/workflows/README.md +0 -81
- package/workflows/_shared/agent-refs.cjs +0 -40
- package/workflows/chain.js +0 -131
- package/workflows/map-reduce.js +0 -174
- package/workflows/parallel.js +0 -148
- package/workflows/review-fix-loop-utils.cjs +0 -1307
- package/workflows/review-fix-loop.js +0 -1396
- package/workflows/scatter-gather.js +0 -178
|
@@ -1,458 +0,0 @@
|
|
|
1
|
-
// src/__tests__/format.test.ts
|
|
2
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
formatElapsedSeconds,
|
|
7
|
-
formatTokens,
|
|
8
|
-
padToVisible,
|
|
9
|
-
sanitizeLabel,
|
|
10
|
-
segFillColored,
|
|
11
|
-
shortId,
|
|
12
|
-
spinnerGlyph,
|
|
13
|
-
statusGlyph,
|
|
14
|
-
truncLine,
|
|
15
|
-
wrapText,
|
|
16
|
-
} from "../../interface/format.ts";
|
|
17
|
-
|
|
18
|
-
// ============================================================
|
|
19
|
-
// formatTokens
|
|
20
|
-
// ============================================================
|
|
21
|
-
describe("formatTokens", () => {
|
|
22
|
-
it("shows plain value below 1000", () => {
|
|
23
|
-
expect(formatTokens(0)).toBe("0");
|
|
24
|
-
expect(formatTokens(820)).toBe("820");
|
|
25
|
-
expect(formatTokens(999)).toBe("999");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("shows N.Nk between 1000 and 9999", () => {
|
|
29
|
-
expect(formatTokens(1000)).toBe("1.0k");
|
|
30
|
-
expect(formatTokens(8200)).toBe("8.2k");
|
|
31
|
-
expect(formatTokens(9999)).toBe("10.0k");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("shows rounded Nk at 10000+", () => {
|
|
35
|
-
expect(formatTokens(10000)).toBe("10k");
|
|
36
|
-
expect(formatTokens(23000)).toBe("23k");
|
|
37
|
-
expect(formatTokens(99999)).toBe("100k");
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// ============================================================
|
|
42
|
-
// formatElapsedSeconds
|
|
43
|
-
// ============================================================
|
|
44
|
-
describe("formatElapsedSeconds", () => {
|
|
45
|
-
it("shows Xs below 60", () => {
|
|
46
|
-
expect(formatElapsedSeconds(0)).toBe("0s");
|
|
47
|
-
expect(formatElapsedSeconds(12)).toBe("12s");
|
|
48
|
-
expect(formatElapsedSeconds(59)).toBe("59s");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("shows Xm Ys between 60 and 3599", () => {
|
|
52
|
-
expect(formatElapsedSeconds(60)).toBe("1m0s");
|
|
53
|
-
expect(formatElapsedSeconds(72)).toBe("1m12s");
|
|
54
|
-
expect(formatElapsedSeconds(3599)).toBe("59m59s");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("shows Xh Ym at 3600+", () => {
|
|
58
|
-
expect(formatElapsedSeconds(3600)).toBe("1h0m");
|
|
59
|
-
expect(formatElapsedSeconds(3661)).toBe("1h1m");
|
|
60
|
-
expect(formatElapsedSeconds(7325)).toBe("2h2m");
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// ============================================================
|
|
65
|
-
// statusGlyph
|
|
66
|
-
// ============================================================
|
|
67
|
-
describe("statusGlyph", () => {
|
|
68
|
-
it("running → no icon, accent color", () => {
|
|
69
|
-
expect(statusGlyph("running")).toEqual({ icon: undefined, color: "accent" });
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("done → checkmark, success", () => {
|
|
73
|
-
expect(statusGlyph("closed")).toEqual({ icon: "✓", color: "success" });
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("closed → checkmark, success", () => {
|
|
77
|
-
expect(statusGlyph("closed")).toEqual({ icon: "✓", color: "success" });
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// ============================================================
|
|
82
|
-
// spinnerGlyph
|
|
83
|
-
// ============================================================
|
|
84
|
-
describe("spinnerGlyph", () => {
|
|
85
|
-
it("returns a frame for valid seed", () => {
|
|
86
|
-
expect(spinnerGlyph(0)).toBe("⠋");
|
|
87
|
-
expect(spinnerGlyph(1)).toBe("⠙");
|
|
88
|
-
expect(spinnerGlyph(9)).toBe("⠏");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("wraps around (mod 10)", () => {
|
|
92
|
-
expect(spinnerGlyph(10)).toBe("⠋");
|
|
93
|
-
expect(spinnerGlyph(15)).toBe("⠴"); // index 5
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("falls back to frame 0 on NaN", () => {
|
|
97
|
-
expect(spinnerGlyph(NaN)).toBe("⠋");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("handles negative seeds via abs", () => {
|
|
101
|
-
expect(spinnerGlyph(-1)).toBe("⠙"); // abs(-1) % 10 = 1 → ⠙
|
|
102
|
-
expect(spinnerGlyph(-10)).toBe("⠋");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("falls back to frame 0 on Infinity", () => {
|
|
106
|
-
expect(spinnerGlyph(Infinity)).toBe("⠋");
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// ============================================================
|
|
111
|
-
// sanitizeLabel
|
|
112
|
-
// ============================================================
|
|
113
|
-
describe("sanitizeLabel", () => {
|
|
114
|
-
it("replaces CRLF/LF with single space", () => {
|
|
115
|
-
expect(sanitizeLabel("line1\r\nline2\nline3")).toBe("line1 line2 line3");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("replaces tabs with 2 spaces", () => {
|
|
119
|
-
expect(sanitizeLabel("a\tb")).toBe("a b");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("collapses multiple consecutive newlines into one space", () => {
|
|
123
|
-
// /[\r\n]+/g treats \r\n\r\n as one match → single space
|
|
124
|
-
expect(sanitizeLabel("a\r\n\r\nb")).toBe("a b");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("leaves clean text unchanged", () => {
|
|
128
|
-
expect(sanitizeLabel("read foo.ts")).toBe("read foo.ts");
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// ============================================================
|
|
133
|
-
// padToVisible
|
|
134
|
-
// ============================================================
|
|
135
|
-
describe("padToVisible", () => {
|
|
136
|
-
it("pads short text to width with trailing spaces", () => {
|
|
137
|
-
expect(padToVisible("ab", 5)).toBe("ab ");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("returns unchanged when already at width", () => {
|
|
141
|
-
expect(padToVisible("hello", 5)).toBe("hello");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("returns unchanged when wider than width", () => {
|
|
145
|
-
expect(padToVisible("hello world", 5)).toBe("hello world");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("handles CJK width (2 columns per char)", () => {
|
|
149
|
-
// 你好 = 4 visible columns
|
|
150
|
-
expect(padToVisible("你好", 6)).toBe("你好 ");
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// ============================================================
|
|
155
|
-
// segFillColored
|
|
156
|
-
// ============================================================
|
|
157
|
-
describe("segFillColored", () => {
|
|
158
|
-
it("returns empty string for width <= 0", () => {
|
|
159
|
-
expect(segFillColored("title", "-", 0)).toBe("");
|
|
160
|
-
expect(segFillColored("title", "-", -1)).toBe("");
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("pure fill when no title", () => {
|
|
164
|
-
expect(segFillColored(undefined, "-", 5)).toBe("-----");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("title + fill to width", () => {
|
|
168
|
-
expect(segFillColored("Hi", "-", 5)).toBe("Hi---");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("truncates title when wider than width", () => {
|
|
172
|
-
const result = segFillColored("Hello World", "-", 5);
|
|
173
|
-
// title visible width 11 > 5 → truncated to 5 (with ellipsis = 4 chars + …)
|
|
174
|
-
expect(result.length).toBeLessThanOrEqual(10); // visible width 5 but may include ANSI
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("preserves ANSI in title and fill separately (no nesting color loss)", () => {
|
|
178
|
-
const redTitle = "\x1b[31mHi\x1b[0m";
|
|
179
|
-
const blueFill = "\x1b[34m-\x1b[0m";
|
|
180
|
-
const result = segFillColored(redTitle, blueFill, 5);
|
|
181
|
-
// title visible width = 2 ("Hi"), fill count = 3
|
|
182
|
-
expect(result).toContain(redTitle);
|
|
183
|
-
expect(result).toContain(blueFill);
|
|
184
|
-
// fill repeated 3 times
|
|
185
|
-
expect(result).toBe(redTitle + blueFill + blueFill + blueFill);
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// ============================================================
|
|
190
|
-
// truncLine
|
|
191
|
-
// ============================================================
|
|
192
|
-
describe("truncLine", () => {
|
|
193
|
-
it("returns text unchanged when within width", () => {
|
|
194
|
-
expect(truncLine("hello", 10)).toBe("hello");
|
|
195
|
-
expect(truncLine("hello", 5)).toBe("hello");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("returns empty string for width <= 0", () => {
|
|
199
|
-
expect(truncLine("hello", 0)).toBe("");
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("truncates with ellipsis when exceeding width", () => {
|
|
203
|
-
const result = truncLine("hello world", 8);
|
|
204
|
-
// 纯文本截断不发 \x1b[0m(全局重置会破坏外层背景色)
|
|
205
|
-
expect(result.endsWith("…")).toBe(true);
|
|
206
|
-
expect(result).not.toContain("\x1b[0m");
|
|
207
|
-
// visible width should be 8 (7 chars + ellipsis)
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("handles CJK characters (2 columns each)", () => {
|
|
211
|
-
// 你好世界 = 8 visible columns; truncate to 5 → 2 chars (4 cols) + …
|
|
212
|
-
const result = truncLine("你好世界", 5);
|
|
213
|
-
expect(result.endsWith("…")).toBe(true);
|
|
214
|
-
expect(result).not.toContain("\x1b[0m");
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("handles emoji correctly", () => {
|
|
218
|
-
const result = truncLine("😀😁😂🤣😃", 3);
|
|
219
|
-
expect(result.endsWith("…")).toBe(true);
|
|
220
|
-
expect(result).not.toContain("\x1b[0m");
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("reapplies active ANSI styles before ellipsis (no background break)", () => {
|
|
224
|
-
// red text that exceeds width → ellipsis should have red re-applied
|
|
225
|
-
const input = "\x1b[31mhello world this is long\x1b[0m";
|
|
226
|
-
const result = truncLine(input, 10);
|
|
227
|
-
expect(result.endsWith("…\x1b[0m")).toBe(true);
|
|
228
|
-
// The ellipsis should be preceded by the active red style (re-applied)
|
|
229
|
-
// Check that the last grapheme sequence includes the red SGR before …
|
|
230
|
-
expect(result).toMatch(/\x1b\[31m…\x1b\[0m$/);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it("clears style stack on reset code", () => {
|
|
234
|
-
// text with reset in the middle → after reset, no style re-applied
|
|
235
|
-
const input = "\x1b[31mab\x1b[0mcdefghijk";
|
|
236
|
-
const result = truncLine(input, 6);
|
|
237
|
-
// reset 后 activeStyles 为空 → 截断不发 \x1b[0m
|
|
238
|
-
expect(result.endsWith("…")).toBe(true);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("flattens newlines to spaces (single-line rendering safety)", () => {
|
|
242
|
-
// 多行 prompt / turn.text 含 \n,单行渲染时 \n 会意外换行破坏行对齐。
|
|
243
|
-
// truncLine 作为单行渲染入口必须剥离 \n(用空格替代保留词边界)。
|
|
244
|
-
const multiLine = "只读任务:分析核心逻辑。\n项目根:/Users/test\n输出:报告";
|
|
245
|
-
const result = truncLine(multiLine, 80);
|
|
246
|
-
expect(result).not.toContain("\n");
|
|
247
|
-
expect(result).toContain("只读任务");
|
|
248
|
-
expect(result).toContain("项目根");
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("flattens \r\n sequences", () => {
|
|
252
|
-
const crlf = "line1\r\nline2\r\nline3";
|
|
253
|
-
const result = truncLine(crlf, 80);
|
|
254
|
-
expect(result).not.toContain("\r");
|
|
255
|
-
expect(result).not.toContain("\n");
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// ============================================================
|
|
260
|
-
// truncLine — legacy 对拍(indexOf 化重构的逐字节等价锚定)
|
|
261
|
-
// ============================================================
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* 旧实现参照:改造前的 per-char 版本,逐字取自 git 历史(093e28fe3~1 的
|
|
265
|
-
* format.ts truncLine)。现行实现(indexOf("\\x1b") 段跳过)必须与其逐字节
|
|
266
|
-
* 等价——本函数即对拍 oracle,与 helpers-bounded-serialize.test.ts 的
|
|
267
|
-
* legacySerialize 同形态(参照物随测试落盘,防「验证只存在于改造当时」)。
|
|
268
|
-
*/
|
|
269
|
-
const legacySegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
270
|
-
|
|
271
|
-
function legacyTruncLine(text: string, maxWidth: number): string {
|
|
272
|
-
if (maxWidth <= 0) return "";
|
|
273
|
-
const flat = text.replace(/[\r\n]+/g, " ");
|
|
274
|
-
if (visibleWidth(flat) <= maxWidth) return flat;
|
|
275
|
-
|
|
276
|
-
const targetWidth = Math.max(0, maxWidth - 1);
|
|
277
|
-
let result = "";
|
|
278
|
-
let currentWidth = 0;
|
|
279
|
-
let activeStyles: string[] = [];
|
|
280
|
-
let i = 0;
|
|
281
|
-
|
|
282
|
-
while (i < flat.length) {
|
|
283
|
-
// 捕获 ANSI SGR 序列
|
|
284
|
-
const ansiMatch = flat.slice(i).match(/^\x1b\[[0-9;]*m/);
|
|
285
|
-
if (ansiMatch) {
|
|
286
|
-
const code = ansiMatch[0];
|
|
287
|
-
result += code;
|
|
288
|
-
|
|
289
|
-
if (code === "\x1b[0m" || code === "\x1b[m") {
|
|
290
|
-
activeStyles = []; // reset → 清空栈
|
|
291
|
-
} else {
|
|
292
|
-
activeStyles.push(code);
|
|
293
|
-
}
|
|
294
|
-
i += code.length;
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// 找到下一段纯文本(非 ANSI)的边界
|
|
299
|
-
let end = i;
|
|
300
|
-
while (end < flat.length && !flat.slice(end).match(/^\x1b\[[0-9;]*m/)) {
|
|
301
|
-
end++;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// 按 grapheme 迭代这段文本,累加到 targetWidth
|
|
305
|
-
const textPortion = flat.slice(i, end);
|
|
306
|
-
for (const seg of legacySegmenter.segment(textPortion)) {
|
|
307
|
-
const grapheme = seg.segment;
|
|
308
|
-
const graphemeWidth = visibleWidth(grapheme);
|
|
309
|
-
|
|
310
|
-
if (currentWidth + graphemeWidth > targetWidth) {
|
|
311
|
-
return result + activeStyles.join("") + "…" + (activeStyles.length ? "\x1b[0m" : "");
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
result += grapheme;
|
|
315
|
-
currentWidth += graphemeWidth;
|
|
316
|
-
}
|
|
317
|
-
i = end;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// 理论上 visibleWidth 检查已提前返回,此行兜底
|
|
321
|
-
return result + activeStyles.join("") + "…" + (activeStyles.length ? "\x1b[0m" : "");
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** 确定性 PRNG(mulberry32):fuzz 用例固定 seed 生成,失败可按 case 编号 + seed 复现。 */
|
|
325
|
-
function mulberry32(seed: number): () => number {
|
|
326
|
-
let s = seed | 0;
|
|
327
|
-
return () => {
|
|
328
|
-
s = (s + 0x6d2b79f5) | 0;
|
|
329
|
-
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
330
|
-
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
331
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** 定向混排片段池:ASCII / CJK / emoji(含肤色修饰与 ZWJ 组合)/ SGR / 非 SGR ESC(OSC、CSI-K、裸 ESC)/ 换行。 */
|
|
336
|
-
const FUZZ_PIECES = [
|
|
337
|
-
"plain", " ", "a", "0", "MiXeD-Case_123",
|
|
338
|
-
"你好世界", "汉字宽度测试", "日本語テキスト",
|
|
339
|
-
"😀", "😁😂🤣", "👍🏽", "👨👩👧👦", "é", "é",
|
|
340
|
-
"\x1b[31m", "\x1b[0m", "\x1b[m", "\x1b[1;32m", "\x1b[38;5;196m",
|
|
341
|
-
"\x1b[K", "\x1b]0;title\x07", "\x1bZ", "\x1b",
|
|
342
|
-
"\n", "\r\n",
|
|
343
|
-
];
|
|
344
|
-
|
|
345
|
-
function genFuzzText(rand: () => number): string {
|
|
346
|
-
const pieceCount = Math.floor(rand() * 40);
|
|
347
|
-
let out = "";
|
|
348
|
-
for (let p = 0; p < pieceCount; p++) {
|
|
349
|
-
out += FUZZ_PIECES[Math.floor(rand() * FUZZ_PIECES.length)];
|
|
350
|
-
}
|
|
351
|
-
return out;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
describe("truncLine — legacy 对拍(per-char 参照逐字节等价)", () => {
|
|
355
|
-
it("定向 fuzz:300 随机 ESC/SGR/CJK/emoji 混排用例(固定 seed 可复现)与旧实现逐字节一致", () => {
|
|
356
|
-
const rand = mulberry32(20260815);
|
|
357
|
-
let truncated = 0;
|
|
358
|
-
for (let i = 0; i < 300; i++) {
|
|
359
|
-
const text = genFuzzText(rand);
|
|
360
|
-
const maxWidth = Math.floor(rand() * 61); // 0..60,含 0/1 边界
|
|
361
|
-
const actual = truncLine(text, maxWidth);
|
|
362
|
-
expect(
|
|
363
|
-
actual,
|
|
364
|
-
`case #${i} text=${JSON.stringify(text)} maxWidth=${maxWidth}`,
|
|
365
|
-
).toBe(legacyTruncLine(text, maxWidth));
|
|
366
|
-
if (actual.endsWith("…")) truncated++;
|
|
367
|
-
}
|
|
368
|
-
// 前置有效性:fuzz 确实命中了截断路径(否则对拍只测了「宽度内原样返回」早退分支)
|
|
369
|
-
expect(truncated).toBeGreaterThan(100);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it("手工对抗用例:SGR 重叠 / OSC 内截断 / CSI-K 混排 / 串尾裸 ESC", () => {
|
|
373
|
-
const cases: Array<[string, number]> = [
|
|
374
|
-
["\x1b[31m红\x1b[32m绿\x1b[1;4m加粗下划线尾巴超宽截断点", 9],
|
|
375
|
-
["AB\x1b]0;title\x07CDEFGHIJKLMNOP", 10],
|
|
376
|
-
["\x1b[31mred\x1b[K\x1b[32mgreen tail past width", 10],
|
|
377
|
-
["text almost at width limit\x1b", 22],
|
|
378
|
-
["before \x1bZ after bare esc tail", 12],
|
|
379
|
-
["\x1b[m你\x1b[0m好\x1b[38;5;196m世👨👩👧👦界".repeat(3), 14],
|
|
380
|
-
];
|
|
381
|
-
for (const [text, maxWidth] of cases) {
|
|
382
|
-
expect(truncLine(text, maxWidth)).toBe(legacyTruncLine(text, maxWidth));
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
// ============================================================
|
|
388
|
-
// wrapText
|
|
389
|
-
// ============================================================
|
|
390
|
-
describe("wrapText", () => {
|
|
391
|
-
it("returns text as-is when shorter than width", () => {
|
|
392
|
-
expect(wrapText("hello", 80)).toEqual(["hello"]);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it("wraps long text into multiple lines (no truncation)", () => {
|
|
396
|
-
const text = "abcdefghij"; // 10 chars
|
|
397
|
-
const lines = wrapText(text, 4);
|
|
398
|
-
// 每行最多 4 列,10 字符 → 3 行(4+4+2),不截断不省略号
|
|
399
|
-
expect(lines.join("")).toBe("abcdefghij");
|
|
400
|
-
for (const l of lines.slice(0, -1)) {
|
|
401
|
-
expect(visibleWidth(l)).toBeLessThanOrEqual(4);
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it("preserves original newlines as paragraph breaks", () => {
|
|
406
|
-
const text = "第一行\n第二行";
|
|
407
|
-
const lines = wrapText(text, 80);
|
|
408
|
-
expect(lines).toEqual(["第一行", "第二行"]);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
it("wraps CJK text correctly (2 columns each)", () => {
|
|
412
|
-
// 你好世界你好世界 = 8 CJK chars = 16 columns, wrap to 4 columns
|
|
413
|
-
const text = "你好世界你好世界";
|
|
414
|
-
const lines = wrapText(text, 4);
|
|
415
|
-
// 每行最多 4 列 = 2 CJK chars, 共 4 行
|
|
416
|
-
expect(lines).toHaveLength(4);
|
|
417
|
-
expect(lines.join("")).toBe(text);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it("handles width <= 0 by returning original", () => {
|
|
421
|
-
expect(wrapText("hello", 0)).toEqual(["hello"]);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it("preserves empty lines from blank paragraphs", () => {
|
|
425
|
-
const text = "a\n\nb";
|
|
426
|
-
expect(wrapText(text, 80)).toEqual(["a", "", "b"]);
|
|
427
|
-
});
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
// ============================================================
|
|
431
|
-
// shortId
|
|
432
|
-
// ============================================================
|
|
433
|
-
describe("shortId", () => {
|
|
434
|
-
it("returns sync id unchanged (run-N already short)", () => {
|
|
435
|
-
expect(shortId("run-1")).toBe("run-1");
|
|
436
|
-
expect(shortId("run-42")).toBe("run-42");
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
it("strips timestamp from background id (bg-tag-seq-<ts> → bg-tag-seq)", () => {
|
|
440
|
-
// 算法回归:多段 id 取前 3 段(SHORT_ID_BG_SEGMENTS)。
|
|
441
|
-
// 注意:当前 subagent ID 已改为 sa-<uuid>(见下面 sa- 用例),workflow ID 为 wf-<ts>-<rand>,
|
|
442
|
-
// 实际不再产生 bg- 形态 id;此处保留作为 shortId 算法的多段降级回归(4 段 → 3 段)。
|
|
443
|
-
expect(shortId("bg-f6f731-10-1719500000000")).toBe("bg-f6f731-10");
|
|
444
|
-
expect(shortId("bg-abc123-99-1719500123456")).toBe("bg-abc123-99");
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
it("handles pure uuid and wf- runId (regression baseline)", () => {
|
|
448
|
-
// 纯 UUID 回归(5段 → 取前3段)
|
|
449
|
-
expect(shortId("550e8400-e29b-41d4-a716-446655440000")).toBe("550e8400-e29b-41d4");
|
|
450
|
-
// wf- 前缀 runId 回归(3段 → 取前3段=原样)
|
|
451
|
-
expect(shortId("wf-1719500000000-a1b2c3")).toBe("wf-1719500000000-a1b2c3");
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
it("keeps sa- prefix for subagent id (sa-<uuid> → sa-<uuid 前3段>)", () => {
|
|
455
|
-
// sa- 前缀 subagent ID(保留前缀 + UUID 前 3 段)
|
|
456
|
-
expect(shortId("sa-550e8400-e29b-41d4-a716-446655440000")).toBe("sa-550e8400-e29b-41d4");
|
|
457
|
-
});
|
|
458
|
-
});
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
// src/execution/__tests__/gc-timer.test.ts
|
|
2
|
-
//
|
|
3
|
-
// [M8] idle record GC 定时器测试(subagent-service.ts startGcTimer,L485-507)。
|
|
4
|
-
//
|
|
5
|
-
// 背景:GC 定时器(1h interval 扫描 + isResumable && idleSince 30 天 TTL + store.archive)
|
|
6
|
-
// 此前零测试覆盖——所有相关测试把 startGcTimer mock 成 no-op(index-session-start /
|
|
7
|
-
// crash-recovery / stream-sink-guard / session-start-reaper / index-session-start-identity)。
|
|
8
|
-
// 误判 isResumable/idleSince 会把可续聊 record 从内存错误归档(用户数据路径),
|
|
9
|
-
// 或永不触发(内存驻留失效)——两个方向都无回归拦截。
|
|
10
|
-
//
|
|
11
|
-
// 本文件用真实 SubagentService + 真实 startGcTimer(不 mock GC 逻辑本身),仅 mock
|
|
12
|
-
// session-runner(import 链依赖 + isResumable 的 hasLiveProcessHandle 查询点)与 logger。
|
|
13
|
-
// timer 全部 vi.useFakeTimers,不真实等待。
|
|
14
|
-
|
|
15
|
-
import type { ChildProcess } from "node:child_process";
|
|
16
|
-
import * as fs from "node:fs";
|
|
17
|
-
import * as os from "node:os";
|
|
18
|
-
import * as path from "node:path";
|
|
19
|
-
|
|
20
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
21
|
-
|
|
22
|
-
const { loggerMock } = vi.hoisted(() => ({
|
|
23
|
-
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn(), info: vi.fn() },
|
|
24
|
-
}));
|
|
25
|
-
vi.mock("@zhushanwen/pi-extension-logger", () => ({ getLogger: () => loggerMock }));
|
|
26
|
-
|
|
27
|
-
// mock session-runner:runSpawn/killAllSpawnedChildren 占位(import 链需要),
|
|
28
|
-
// getChildByRecord 默认返回 undefined(无活进程 → isResumable=true),用例内按需覆盖。
|
|
29
|
-
vi.mock("../session-runner.ts", () => ({
|
|
30
|
-
runSpawn: vi.fn(),
|
|
31
|
-
killAllSpawnedChildren: vi.fn(),
|
|
32
|
-
getChildByRecord: vi.fn(() => undefined),
|
|
33
|
-
spawnedChildren: new Map(),
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
import { getChildByRecord } from "../session-runner.ts";
|
|
37
|
-
import { createRecord } from "../execution-record.ts";
|
|
38
|
-
import { ModelConfigService } from "../model-config-service.ts";
|
|
39
|
-
import { RecordStore } from "../record-store.ts";
|
|
40
|
-
import { SubagentService } from "../subagent-service.ts";
|
|
41
|
-
import type { ExecutionRecord } from "../types.ts";
|
|
42
|
-
|
|
43
|
-
const mockGetChildByRecord = vi.mocked(getChildByRecord);
|
|
44
|
-
|
|
45
|
-
/** 与 startGcTimer 内部常量一致(1h 扫描 / 30 天 TTL)。 */
|
|
46
|
-
const GC_INTERVAL_MS = 60 * 60 * 1000;
|
|
47
|
-
const IDLE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
48
|
-
|
|
49
|
-
/** initSession 注入的最小 pi duck-type(同 subagent-service PiLike 形状,结构匹配即可)。 */
|
|
50
|
-
interface PiStub {
|
|
51
|
-
appendEntry(customType: string, data?: unknown): void;
|
|
52
|
-
events: { emit(channel: string, data: unknown): void };
|
|
53
|
-
sendMessage(
|
|
54
|
-
message: { customType: string; content: string; display: boolean; details?: unknown },
|
|
55
|
-
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
56
|
-
): void;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function makePi(): PiStub {
|
|
60
|
-
return {
|
|
61
|
-
appendEntry: vi.fn(),
|
|
62
|
-
events: { emit: vi.fn() },
|
|
63
|
-
sendMessage: vi.fn(),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 构造 running + 无活进程 + 指定 idleSince 的 record(GC 扫描的目标态)。
|
|
68
|
-
* listAllActive 只返回 status==="running"(createRecord 初始即 running)。 */
|
|
69
|
-
function makeIdleRecord(id: string, idleSince: number): ExecutionRecord {
|
|
70
|
-
const record = createRecord(id, {
|
|
71
|
-
agent: "general-purpose",
|
|
72
|
-
model: "test/model",
|
|
73
|
-
mode: "background",
|
|
74
|
-
task: "test",
|
|
75
|
-
slug: "test",
|
|
76
|
-
startedAt: idleSince,
|
|
77
|
-
rootSessionId: "root-session",
|
|
78
|
-
chatMode: true,
|
|
79
|
-
controller: new AbortController(),
|
|
80
|
-
});
|
|
81
|
-
record.idleSince = idleSince;
|
|
82
|
-
return record;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** 暴露私有字段供测试读取 store。 */
|
|
86
|
-
interface ServiceInternals {
|
|
87
|
-
store: RecordStore;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
describe("[M8] idle record GC 定时器(startGcTimer)", () => {
|
|
91
|
-
let agentDir: string;
|
|
92
|
-
let service: SubagentService;
|
|
93
|
-
let store: RecordStore;
|
|
94
|
-
|
|
95
|
-
beforeEach(() => {
|
|
96
|
-
vi.useFakeTimers();
|
|
97
|
-
agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "gc-timer-"));
|
|
98
|
-
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
99
|
-
service = new SubagentService({ cwd: agentDir, modelService });
|
|
100
|
-
service.initSession({ pi: makePi(), sessionId: "root-session" });
|
|
101
|
-
store = (service as unknown as ServiceInternals).store;
|
|
102
|
-
mockGetChildByRecord.mockReset();
|
|
103
|
-
// 默认无活进程(isResumable=true)——Path B:进程已回收、可冷路径 resume 的等待续聊态
|
|
104
|
-
mockGetChildByRecord.mockImplementation(() => undefined);
|
|
105
|
-
loggerMock.warn.mockClear();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
afterEach(() => {
|
|
109
|
-
service.dispose();
|
|
110
|
-
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
111
|
-
vi.useRealTimers();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("TTL 边界:超 1ms 归档、差 1ms 不归档(同一轮扫描)", () => {
|
|
115
|
-
// idleSince 相对 interval 首次触发时刻(fake clock + 1h)构造:
|
|
116
|
-
// over = fireAt - TTL - 1 → 扫描时 age = TTL + 1 > TTL → archive
|
|
117
|
-
// under = fireAt - TTL + 1 → 扫描时 age = TTL - 1 ≤ TTL → 留内存
|
|
118
|
-
const fireAt = Date.now() + GC_INTERVAL_MS;
|
|
119
|
-
store.register(makeIdleRecord("sa-over", fireAt - IDLE_TTL_MS - 1));
|
|
120
|
-
store.register(makeIdleRecord("sa-under", fireAt - IDLE_TTL_MS + 1));
|
|
121
|
-
|
|
122
|
-
service.startGcTimer();
|
|
123
|
-
vi.advanceTimersByTime(GC_INTERVAL_MS);
|
|
124
|
-
|
|
125
|
-
expect(store.getMutable("sa-over")).toBeUndefined(); // archive 从内存移除
|
|
126
|
-
expect(store.getMutable("sa-under")).toBeDefined(); // 未超 TTL 留内存
|
|
127
|
-
expect(loggerMock.warn).toHaveBeenCalledTimes(1);
|
|
128
|
-
expect(loggerMock.warn).toHaveBeenCalledWith(
|
|
129
|
-
expect.stringContaining("GC: archiving idle record sa-over"),
|
|
130
|
-
);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("有活进程的 record 不归档(isResumable=false,即使 idleSince 超 TTL)", () => {
|
|
134
|
-
const fireAt = Date.now() + GC_INTERVAL_MS;
|
|
135
|
-
// Path A:进程保活等待续聊(idle timer armed、child 未 kill)——归档会让活进程失管
|
|
136
|
-
store.register(makeIdleRecord("sa-live", fireAt - IDLE_TTL_MS - 1));
|
|
137
|
-
mockGetChildByRecord.mockImplementation(
|
|
138
|
-
(id: string): ChildProcess | undefined =>
|
|
139
|
-
id === "sa-live" ? ({ killed: false } as unknown as ChildProcess) : undefined,
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
service.startGcTimer();
|
|
143
|
-
vi.advanceTimersByTime(GC_INTERVAL_MS);
|
|
144
|
-
|
|
145
|
-
expect(store.getMutable("sa-live")).toBeDefined(); // 有活进程 → 不归档
|
|
146
|
-
expect(loggerMock.warn).not.toHaveBeenCalled();
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("每小时重复扫描:上一轮差 1ms 的 record 下一轮超龄被归档", () => {
|
|
150
|
-
const fireAt = Date.now() + GC_INTERVAL_MS;
|
|
151
|
-
store.register(makeIdleRecord("sa-late", fireAt - IDLE_TTL_MS + 1));
|
|
152
|
-
|
|
153
|
-
service.startGcTimer();
|
|
154
|
-
vi.advanceTimersByTime(GC_INTERVAL_MS);
|
|
155
|
-
expect(store.getMutable("sa-late")).toBeDefined(); // 第一轮:age = TTL - 1,未到
|
|
156
|
-
|
|
157
|
-
vi.advanceTimersByTime(GC_INTERVAL_MS); // 第二轮:age = TTL + 1h - 1 > TTL
|
|
158
|
-
expect(store.getMutable("sa-late")).toBeUndefined();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("startGcTimer 幂等:重复调用不产生第二个 interval", () => {
|
|
162
|
-
const fireAt = Date.now() + GC_INTERVAL_MS;
|
|
163
|
-
store.register(makeIdleRecord("sa-over", fireAt - IDLE_TTL_MS - 1));
|
|
164
|
-
|
|
165
|
-
service.startGcTimer();
|
|
166
|
-
service.startGcTimer(); // 已有 timer 直接 return(防重复 interval)
|
|
167
|
-
vi.advanceTimersByTime(GC_INTERVAL_MS);
|
|
168
|
-
|
|
169
|
-
// 同一 record 一轮只扫一次(重复 interval 会对同一 record 产生第二条 GC warn)
|
|
170
|
-
expect(loggerMock.warn).toHaveBeenCalledTimes(1);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("dispose 后 interval 停止:超 TTL record 不再被归档", () => {
|
|
174
|
-
service.startGcTimer();
|
|
175
|
-
vi.advanceTimersByTime(GC_INTERVAL_MS); // 第一轮正常触发
|
|
176
|
-
service.dispose(); // stopGcTimer + disposeAllRecords
|
|
177
|
-
|
|
178
|
-
// dispose 后塞入超 TTL record,interval 已停 → 无论推进多久都不归档
|
|
179
|
-
store.register(makeIdleRecord("sa-after-dispose", Date.now() - IDLE_TTL_MS - 1));
|
|
180
|
-
vi.advanceTimersByTime(GC_INTERVAL_MS * 10);
|
|
181
|
-
|
|
182
|
-
expect(store.getMutable("sa-after-dispose")).toBeDefined();
|
|
183
|
-
});
|
|
184
|
-
});
|