tmux-ide 2.9.2 → 2.9.4
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/LICENSE +1 -1
- package/README.md +121 -140
- package/bin/cli.js +2457 -727
- package/package.json +23 -8
- package/packages/contracts/src/desktop-host.ts +3 -0
- package/packages/contracts/src/saved-machines.ts +19 -0
- package/packages/contracts/src/terminal-replica.ts +1 -1
- package/packages/core/package.json +3 -2
- package/packages/core/src/terminal-delivery.ts +51 -3
- package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
- package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
- package/packages/core/src/terminal-replica-owned-row.ts +27 -0
- package/packages/core/src/terminal-replica.ts +13 -28
- package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
- package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
- package/packages/daemon/dist/command-center/ws-events.js +5 -24
- package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
- package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
- package/packages/daemon/dist/lib/daemon-embed.js +1 -1
- package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
- package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
- package/packages/daemon/dist/lib/daemon-service.js +259 -0
- package/packages/daemon/dist/lib/install-origin.js +2 -0
- package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
- package/packages/daemon/dist/lib/log.js +6 -10
- package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
- package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
- package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
- package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
- package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
- package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
- package/packages/daemon/dist/lib/saved-machines.js +40 -2
- package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
- package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
- package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
- package/packages/daemon/dist/lib/tui-binary.js +2 -2
- package/packages/daemon/dist/lib/update.js +18 -0
- package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
- package/packages/daemon/dist/machines.js +40 -4
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
- package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
- package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
- package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
- package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
- package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
- package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
- package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
- package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
- package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
- package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
- package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
- package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
- package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
- package/packages/daemon/src/command-center/ws-events.ts +5 -25
- package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
- package/packages/daemon/src/lib/daemon-embed.ts +1 -0
- package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
- package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
- package/packages/daemon/src/lib/daemon-service.ts +301 -0
- package/packages/daemon/src/lib/install-origin.ts +5 -0
- package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
- package/packages/daemon/src/lib/log.ts +13 -9
- package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
- package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
- package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
- package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
- package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
- package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
- package/packages/daemon/src/lib/saved-machines.ts +40 -1
- package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
- package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
- package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
- package/packages/daemon/src/lib/tui-binary.ts +2 -2
- package/packages/daemon/src/lib/update.ts +24 -0
- package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
- package/packages/daemon/src/machines.ts +53 -5
- package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
- package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
- package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
- package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
- package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
- package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
- package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
- package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
- package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
- package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
- package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
- package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
- package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
- package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
- package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
- package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
- package/packages/daemon-client/src/workspace-client.ts +4 -1
- package/skill/SKILL.md +30 -30
- package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
- package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
- package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
- package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
- package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
- package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
- package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
- package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
- package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
- package/packages/contracts/src/__tests__/commands.test.ts +0 -86
- package/packages/contracts/src/__tests__/control.test.ts +0 -154
- package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
- package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
- package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
- package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
- package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
- package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
- package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
- package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
- package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
- package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
- package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
- package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
- package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
- package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
- package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
- package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
- package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
- package/packages/contracts/src/__tests__/missions.test.ts +0 -272
- package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
- package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
- package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
- package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
- package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
- package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
- package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
- package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
- package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
- package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
- package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
- package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
- package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
- package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
- package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
- package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
- package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
- package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
- package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
- package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
- package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
- package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
- package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
- package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
- package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
- package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
- package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
- package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
- package/packages/contracts/src/native-operation-identity.test.ts +0 -23
- package/packages/contracts/src/workspace-admission.test.ts +0 -21
- package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
- package/packages/core/src/agent-provisioning.test.ts +0 -42
- package/packages/core/src/application-shell-session.test.ts +0 -105
- package/packages/core/src/interaction-evidence.test.ts +0 -134
- package/packages/core/src/interaction-flow.test.ts +0 -66
- package/packages/core/src/interaction-receipts.test.ts +0 -671
- package/packages/core/src/navigator.test.ts +0 -34
- package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
- package/packages/core/src/optimistic-projection.test.ts +0 -118
- package/packages/core/src/performance-metrics.test.ts +0 -121
- package/packages/core/src/performance-qualification.test.ts +0 -283
- package/packages/core/src/saved-machines.test.ts +0 -80
- package/packages/core/src/terminal-compact-cold.test.ts +0 -87
- package/packages/core/src/terminal-conformance.test.ts +0 -48
- package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
- package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
- package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
- package/packages/core/src/terminal-delivery.test.ts +0 -1471
- package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
- package/packages/core/src/terminal-replica-cold.test.ts +0 -31
- package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
- package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
- package/packages/core/src/terminal-replica.test.ts +0 -347
- package/packages/core/src/workspace-selection.test.ts +0 -63
- package/packages/daemon/dist/__tests__/support.js +0 -33
- package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
- package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
- package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
- package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
- package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
- package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
- package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
- package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
- package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
- package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
- package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
- package/packages/daemon-client/src/automation-client.test.ts +0 -214
- package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
- package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
- package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
- package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
- package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
- package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
- package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
- package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
- package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
- package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
- package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
- package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
- package/packages/daemon-client/src/resource-replica.test.ts +0 -186
- package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
- package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
- package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
- package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
- package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
- package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
- package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
- package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
- package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
- package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
- package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
- package/packages/presentation/src/presentation-contract.test.ts +0 -63
- package/packages/tmux-bridge/src/runner.test.ts +0 -614
- package/packages/tmux-bridge/src/targeting.test.ts +0 -107
|
@@ -1,2912 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
APPLICATION_SHELL_COMMAND_IDS,
|
|
4
|
-
APPLICATION_SHELL_RESOURCE_V3_VERSION,
|
|
5
|
-
ApplicationShellProjectionInputV3SchemaZ,
|
|
6
|
-
COHESION_FIXTURE_V1,
|
|
7
|
-
DAEMON_WIRE_PROTOCOL_VERSION,
|
|
8
|
-
DesktopApplicationShellTargetSchemaZ,
|
|
9
|
-
applicationShellCommandInvocation,
|
|
10
|
-
type ApplicationShellProjectionInputV3,
|
|
11
|
-
type DesktopApplicationShellTarget,
|
|
12
|
-
type InteractionReceipt,
|
|
13
|
-
type SessionRuntimeTerminalInput,
|
|
14
|
-
type TerminalReplicaAddress,
|
|
15
|
-
type TerminalReplicaUpdate,
|
|
16
|
-
} from "@tmux-ide/contracts";
|
|
17
|
-
|
|
18
|
-
import type {
|
|
19
|
-
WorkspaceClient,
|
|
20
|
-
ApplicationShellEventHandlers,
|
|
21
|
-
ApplicationShellTransport,
|
|
22
|
-
} from "./application-shell-session.ts";
|
|
23
|
-
import type { GenerationBoundClock } from "./generation-bound-store.ts";
|
|
24
|
-
import {
|
|
25
|
-
createDirectLoopbackDaemonTransport,
|
|
26
|
-
type TerminalFirstDaemonTransport,
|
|
27
|
-
} from "./direct-application-shell-transport.ts";
|
|
28
|
-
import type {
|
|
29
|
-
WorkspaceEventSocket,
|
|
30
|
-
WorkspaceEventSocketEvent,
|
|
31
|
-
WorkspaceEventSocketEventType,
|
|
32
|
-
WorkspaceEventSocketListener,
|
|
33
|
-
} from "./workspace-event-supervisor.ts";
|
|
34
|
-
import { createWorkspaceClient } from "./workspace-client.ts";
|
|
35
|
-
import { runtimeResourceSnapshot } from "./runtime-resource-ledger.ts";
|
|
36
|
-
import {
|
|
37
|
-
createTerminalFastLane,
|
|
38
|
-
createWorkspaceClientTerminalSource,
|
|
39
|
-
type TerminalFastLane,
|
|
40
|
-
} from "./terminal-fast-lane.ts";
|
|
41
|
-
import { createWorkspaceClientConformanceAdapter } from "./workspace-client-conformance.ts";
|
|
42
|
-
import type {
|
|
43
|
-
WorkspaceClientOwnerActionPort,
|
|
44
|
-
WorkspaceClientRuntimeInventory,
|
|
45
|
-
WorkspaceClientRuntimePort,
|
|
46
|
-
} from "./workspace-client-types.ts";
|
|
47
|
-
|
|
48
|
-
const daemon = (instanceId: string) => ({
|
|
49
|
-
protocolVersion: DAEMON_WIRE_PROTOCOL_VERSION,
|
|
50
|
-
productVersion: "2.8.0",
|
|
51
|
-
instanceId,
|
|
52
|
-
startedAt: "2026-08-14T10:00:00.000Z",
|
|
53
|
-
});
|
|
54
|
-
const ALPHA_DAEMON = daemon("11111111-1111-4111-8111-111111111111");
|
|
55
|
-
const BETA_DAEMON = daemon("22222222-2222-4222-8222-222222222222");
|
|
56
|
-
|
|
57
|
-
function target(workspaceName: string, identity = ALPHA_DAEMON): DesktopApplicationShellTarget {
|
|
58
|
-
return DesktopApplicationShellTargetSchemaZ.parse({ daemon: identity, workspaceName });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function workspaceCatalog(workspaceName: string, identity = ALPHA_DAEMON) {
|
|
62
|
-
return {
|
|
63
|
-
version: 2,
|
|
64
|
-
daemon: identity,
|
|
65
|
-
intents: [
|
|
66
|
-
{
|
|
67
|
-
workspaceName,
|
|
68
|
-
sessionName: workspaceName,
|
|
69
|
-
source: "workspace",
|
|
70
|
-
availability: "live",
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
liveSessions: [
|
|
74
|
-
{
|
|
75
|
-
sessionName: workspaceName,
|
|
76
|
-
fleetSessionId: `session.${workspaceName === "alpha" ? "a" : "b"}`.padEnd(28, "0"),
|
|
77
|
-
paneCount: 1,
|
|
78
|
-
},
|
|
79
|
-
],
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function shellResource(
|
|
84
|
-
name: string,
|
|
85
|
-
semanticPaneIds: readonly string[] = [`pane.${name}`],
|
|
86
|
-
): ApplicationShellProjectionInputV3 {
|
|
87
|
-
return ApplicationShellProjectionInputV3SchemaZ.parse({
|
|
88
|
-
project: { ...COHESION_FIXTURE_V1.project, name },
|
|
89
|
-
workspace: {
|
|
90
|
-
...COHESION_FIXTURE_V1.workspace,
|
|
91
|
-
id: `workspace.${name}`,
|
|
92
|
-
activeMode: "terminals",
|
|
93
|
-
sidebar: {
|
|
94
|
-
...COHESION_FIXTURE_V1.workspace.sidebar,
|
|
95
|
-
agents: COHESION_FIXTURE_V1.workspace.sidebar.agents.map((agent) => ({
|
|
96
|
-
...agent,
|
|
97
|
-
paneId: null,
|
|
98
|
-
})),
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
dock: COHESION_FIXTURE_V1.dock,
|
|
102
|
-
focus: { ...COHESION_FIXTURE_V1.focus, overlays: [] },
|
|
103
|
-
connection: COHESION_FIXTURE_V1.connection,
|
|
104
|
-
terminalInventory: {
|
|
105
|
-
activeResourceId: semanticPaneIds[0] ?? null,
|
|
106
|
-
resources: semanticPaneIds.map((semanticPaneId, index) => ({
|
|
107
|
-
id: semanticPaneId,
|
|
108
|
-
title: `Terminal ${index + 1}`,
|
|
109
|
-
kind: "terminal" as const,
|
|
110
|
-
active: index === 0,
|
|
111
|
-
attachability: { status: "available" as const, semanticPaneId },
|
|
112
|
-
interactionEndpoint: null,
|
|
113
|
-
nativeIdentity: null,
|
|
114
|
-
})),
|
|
115
|
-
},
|
|
116
|
-
appWindows: {
|
|
117
|
-
version: 1,
|
|
118
|
-
revision: 7,
|
|
119
|
-
updatedAt: "2026-08-14T10:00:00.000Z",
|
|
120
|
-
windows: {},
|
|
121
|
-
dockRoot: null,
|
|
122
|
-
dockState: { mode: "collapsed", preferredHeight: null, focusZone: "canvas" },
|
|
123
|
-
floatingOrder: [],
|
|
124
|
-
focusedWindowId: null,
|
|
125
|
-
activeLayoutId: null,
|
|
126
|
-
layouts: {},
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
class FakeClock implements GenerationBoundClock {
|
|
132
|
-
nowValue = 1_000;
|
|
133
|
-
nextId = 0;
|
|
134
|
-
readonly timers = new Map<number, { at: number; callback: () => void }>();
|
|
135
|
-
now(): number {
|
|
136
|
-
return this.nowValue;
|
|
137
|
-
}
|
|
138
|
-
setTimeout(callback: () => void, delayMs: number): unknown {
|
|
139
|
-
const id = ++this.nextId;
|
|
140
|
-
this.timers.set(id, { at: this.nowValue + delayMs, callback });
|
|
141
|
-
return id;
|
|
142
|
-
}
|
|
143
|
-
clearTimeout(handle: unknown): void {
|
|
144
|
-
this.timers.delete(handle as number);
|
|
145
|
-
}
|
|
146
|
-
advance(ms: number): void {
|
|
147
|
-
this.nowValue += ms;
|
|
148
|
-
for (;;) {
|
|
149
|
-
const due = [...this.timers.entries()]
|
|
150
|
-
.filter(([, timer]) => timer.at <= this.nowValue)
|
|
151
|
-
.sort((left, right) => left[1].at - right[1].at)[0];
|
|
152
|
-
if (due === undefined) return;
|
|
153
|
-
this.timers.delete(due[0]);
|
|
154
|
-
due[1].callback();
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
interface ShellConnection {
|
|
160
|
-
readonly target: DesktopApplicationShellTarget;
|
|
161
|
-
readonly handlers: ApplicationShellEventHandlers;
|
|
162
|
-
closed: boolean;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function shellBroker(resources: Readonly<Record<string, ApplicationShellProjectionInputV3>>) {
|
|
166
|
-
const byWorkspace = new Map(Object.entries(resources));
|
|
167
|
-
const connections: ShellConnection[] = [];
|
|
168
|
-
const transport: ApplicationShellTransport<ApplicationShellProjectionInputV3> = {
|
|
169
|
-
validateTarget: (value) => DesktopApplicationShellTargetSchemaZ.parse(value),
|
|
170
|
-
fetchApplicationShell: async (current) => {
|
|
171
|
-
const resource = byWorkspace.get(current.workspaceName);
|
|
172
|
-
if (resource === undefined) throw new Error(`missing ${current.workspaceName}`);
|
|
173
|
-
return resource;
|
|
174
|
-
},
|
|
175
|
-
connectEvents: (current, handlers) => {
|
|
176
|
-
const connection = { target: current, handlers, closed: false };
|
|
177
|
-
connections.push(connection);
|
|
178
|
-
return { close: () => (connection.closed = true) };
|
|
179
|
-
},
|
|
180
|
-
};
|
|
181
|
-
return { transport, connections, byWorkspace };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
class WorkspaceClientEventSocket implements WorkspaceEventSocket {
|
|
185
|
-
readyState = 1;
|
|
186
|
-
readonly sent: unknown[] = [];
|
|
187
|
-
readonly listeners = new Map<WorkspaceEventSocketEventType, Set<WorkspaceEventSocketListener>>();
|
|
188
|
-
addEventListener(type: WorkspaceEventSocketEventType, listener: WorkspaceEventSocketListener) {
|
|
189
|
-
const listeners = this.listeners.get(type) ?? new Set();
|
|
190
|
-
listeners.add(listener);
|
|
191
|
-
this.listeners.set(type, listeners);
|
|
192
|
-
}
|
|
193
|
-
removeEventListener(type: WorkspaceEventSocketEventType, listener: WorkspaceEventSocketListener) {
|
|
194
|
-
this.listeners.get(type)?.delete(listener);
|
|
195
|
-
}
|
|
196
|
-
send(value: string) {
|
|
197
|
-
this.sent.push(JSON.parse(value));
|
|
198
|
-
}
|
|
199
|
-
close() {}
|
|
200
|
-
emit(type: WorkspaceEventSocketEventType, event: WorkspaceEventSocketEvent = {}) {
|
|
201
|
-
for (const listener of this.listeners.get(type) ?? []) listener(event);
|
|
202
|
-
}
|
|
203
|
-
frame(value: unknown) {
|
|
204
|
-
this.emit("message", { data: JSON.stringify(value) });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
class FakeTerminalSubscription {
|
|
209
|
-
readonly listeners = new Set<(update: TerminalReplicaUpdate<string, string>) => void>();
|
|
210
|
-
closeCount = 0;
|
|
211
|
-
closeGate: Promise<void> | null = null;
|
|
212
|
-
constructor(readonly generation: string) {}
|
|
213
|
-
async close(): Promise<void> {
|
|
214
|
-
this.closeCount += 1;
|
|
215
|
-
if (this.closeGate) await this.closeGate;
|
|
216
|
-
}
|
|
217
|
-
freeze(): void {}
|
|
218
|
-
thaw(): void {}
|
|
219
|
-
onUpdate(listener: (update: TerminalReplicaUpdate<string, string>) => void): () => void {
|
|
220
|
-
this.listeners.add(listener);
|
|
221
|
-
return () => this.listeners.delete(listener);
|
|
222
|
-
}
|
|
223
|
-
emit(update: TerminalReplicaUpdate<string, string>): void {
|
|
224
|
-
for (const listener of this.listeners) listener(update);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
class FakeRuntime implements WorkspaceClientRuntimePort<string, string> {
|
|
229
|
-
readonly closed = new Promise<never>(() => undefined);
|
|
230
|
-
readonly receipts = new Set<(receipt: InteractionReceipt) => void>();
|
|
231
|
-
readonly subscriptions = new Map<string, FakeTerminalSubscription>();
|
|
232
|
-
closeCount = 0;
|
|
233
|
-
submitCount = 0;
|
|
234
|
-
readonly inputs: Array<{
|
|
235
|
-
target: TerminalReplicaAddress;
|
|
236
|
-
input: SessionRuntimeTerminalInput;
|
|
237
|
-
traceId?: string;
|
|
238
|
-
}> = [];
|
|
239
|
-
inputResult: "ok" | "authority-lost" = "ok";
|
|
240
|
-
inputGate: Promise<void> | null = null;
|
|
241
|
-
readonly viewportFits: Array<{ cols: number; rows: number; semanticWindowId?: string }> = [];
|
|
242
|
-
viewportGate: Promise<void> | null = null;
|
|
243
|
-
viewportResult: "ok" | "geometry-authority-conflict" = "ok";
|
|
244
|
-
authorityGate: Promise<void> | null = null;
|
|
245
|
-
closeGate: Promise<void> | null = null;
|
|
246
|
-
readonly repairRequests: Array<{
|
|
247
|
-
target: TerminalReplicaAddress;
|
|
248
|
-
reason: "gap" | "conflict" | "wrong-address" | "missing-state";
|
|
249
|
-
}> = [];
|
|
250
|
-
constructor(readonly generation: string) {}
|
|
251
|
-
async subscribeTerminal(address: TerminalReplicaAddress): Promise<FakeTerminalSubscription> {
|
|
252
|
-
const subscription = new FakeTerminalSubscription(this.generation);
|
|
253
|
-
this.subscriptions.set(address.semanticPaneId, subscription);
|
|
254
|
-
return subscription;
|
|
255
|
-
}
|
|
256
|
-
async submitIntent(): Promise<void> {
|
|
257
|
-
this.submitCount += 1;
|
|
258
|
-
}
|
|
259
|
-
async sendTerminalInput(
|
|
260
|
-
nextTarget: TerminalReplicaAddress,
|
|
261
|
-
input: SessionRuntimeTerminalInput,
|
|
262
|
-
traceId?: string,
|
|
263
|
-
): Promise<"ok" | "authority-lost"> {
|
|
264
|
-
if (this.inputGate) await this.inputGate;
|
|
265
|
-
this.inputs.push({ target: nextTarget, input, ...(traceId ? { traceId } : {}) });
|
|
266
|
-
return this.inputResult;
|
|
267
|
-
}
|
|
268
|
-
async fitViewport(
|
|
269
|
-
cols: number,
|
|
270
|
-
rows: number,
|
|
271
|
-
semanticWindowId?: string,
|
|
272
|
-
): Promise<"ok" | "geometry-authority-conflict"> {
|
|
273
|
-
if (this.viewportGate) await this.viewportGate;
|
|
274
|
-
this.viewportFits.push({
|
|
275
|
-
cols,
|
|
276
|
-
rows,
|
|
277
|
-
...(semanticWindowId === undefined ? {} : { semanticWindowId }),
|
|
278
|
-
});
|
|
279
|
-
return this.viewportResult;
|
|
280
|
-
}
|
|
281
|
-
async requestAuthority(authority: "input" | "focus" | "geometry") {
|
|
282
|
-
if (this.authorityGate) await this.authorityGate;
|
|
283
|
-
return {
|
|
284
|
-
generation: this.generation,
|
|
285
|
-
session: "alpha",
|
|
286
|
-
clientId: "opentui:test",
|
|
287
|
-
authority,
|
|
288
|
-
token: "55555555-5555-4555-8555-555555555555",
|
|
289
|
-
revision: 1,
|
|
290
|
-
} as const;
|
|
291
|
-
}
|
|
292
|
-
onReceipt(listener: (receipt: InteractionReceipt) => void): () => void {
|
|
293
|
-
this.receipts.add(listener);
|
|
294
|
-
return () => this.receipts.delete(listener);
|
|
295
|
-
}
|
|
296
|
-
emitReceipt(receipt: InteractionReceipt): void {
|
|
297
|
-
for (const listener of this.receipts) listener(receipt);
|
|
298
|
-
}
|
|
299
|
-
requestTerminalRepair = (
|
|
300
|
-
nextTarget: TerminalReplicaAddress,
|
|
301
|
-
reason: "gap" | "conflict" | "wrong-address" | "missing-state",
|
|
302
|
-
): void => {
|
|
303
|
-
this.repairRequests.push({ target: nextTarget, reason });
|
|
304
|
-
};
|
|
305
|
-
async close(): Promise<void> {
|
|
306
|
-
this.closeCount += 1;
|
|
307
|
-
if (this.closeGate) await this.closeGate;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
class EagerSeedRuntime extends FakeRuntime {
|
|
312
|
-
constructor(
|
|
313
|
-
generation: string,
|
|
314
|
-
private readonly snapshot: string,
|
|
315
|
-
) {
|
|
316
|
-
super(generation);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
override async subscribeTerminal(
|
|
320
|
-
address: TerminalReplicaAddress,
|
|
321
|
-
): Promise<FakeTerminalSubscription> {
|
|
322
|
-
const subscription = new FakeTerminalSubscription(this.generation);
|
|
323
|
-
const onUpdate = subscription.onUpdate.bind(subscription);
|
|
324
|
-
subscription.onUpdate = (listener) => {
|
|
325
|
-
const unsubscribe = onUpdate(listener);
|
|
326
|
-
listener({
|
|
327
|
-
...terminalSeedFor(address.semanticPaneId, this.generation),
|
|
328
|
-
snapshot: this.snapshot,
|
|
329
|
-
});
|
|
330
|
-
return unsubscribe;
|
|
331
|
-
};
|
|
332
|
-
this.subscriptions.set(address.semanticPaneId, subscription);
|
|
333
|
-
return subscription;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
class ClosableFakeRuntime extends FakeRuntime {
|
|
338
|
-
private readonly closedState = deferred<void>();
|
|
339
|
-
override readonly closed = this.closedState.promise;
|
|
340
|
-
fail(): void {
|
|
341
|
-
this.closedState.resolve();
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const actions: WorkspaceClientOwnerActionPort = {
|
|
346
|
-
dispatch: async () => null,
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
async function settle(): Promise<void> {
|
|
350
|
-
for (let turn = 0; turn < 16; turn += 1) await Promise.resolve();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function deferred<T>() {
|
|
354
|
-
let resolve!: (value: T) => void;
|
|
355
|
-
const promise = new Promise<T>((accept) => {
|
|
356
|
-
resolve = accept;
|
|
357
|
-
});
|
|
358
|
-
return { promise, resolve };
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function receipt(operationId: string, phase: InteractionReceipt["phase"]): InteractionReceipt {
|
|
362
|
-
return {
|
|
363
|
-
evidence: null,
|
|
364
|
-
type: "interaction.receipt",
|
|
365
|
-
sequence: phase === "accepted" ? 1 : 2,
|
|
366
|
-
operationId,
|
|
367
|
-
origin: "sdk",
|
|
368
|
-
workspaceName: "alpha",
|
|
369
|
-
sourceSemanticPaneId: null,
|
|
370
|
-
target: { kind: "pane", semanticPaneId: "pane.alpha" },
|
|
371
|
-
operationKind: "workspace.pane.send",
|
|
372
|
-
phase,
|
|
373
|
-
summary: {
|
|
374
|
-
operationKind: "workspace.pane.send",
|
|
375
|
-
characterCount: 1,
|
|
376
|
-
byteCount: 1,
|
|
377
|
-
submitted: true,
|
|
378
|
-
},
|
|
379
|
-
proof:
|
|
380
|
-
phase === "observed"
|
|
381
|
-
? {
|
|
382
|
-
operationKind: "workspace.pane.send",
|
|
383
|
-
observed: true,
|
|
384
|
-
semanticPaneId: "pane.alpha",
|
|
385
|
-
}
|
|
386
|
-
: null,
|
|
387
|
-
at: "2026-08-14T10:00:00.000Z",
|
|
388
|
-
resourceRevision: null,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function terminalSeed(generation: string): TerminalReplicaUpdate<string, string> {
|
|
393
|
-
return {
|
|
394
|
-
type: "terminal.seed",
|
|
395
|
-
workspaceName: "alpha",
|
|
396
|
-
semanticPaneId: "pane.alpha",
|
|
397
|
-
generation,
|
|
398
|
-
incarnation: "incarnation-1",
|
|
399
|
-
revision: 0,
|
|
400
|
-
cols: 80,
|
|
401
|
-
rows: 24,
|
|
402
|
-
stateHash: "0000000000000000",
|
|
403
|
-
hashAlgorithm: "fnv1a64-v1",
|
|
404
|
-
snapshot: "terminal bytes stay outside semantic snapshots",
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function terminalSeedFor(
|
|
409
|
-
semanticPaneId: string,
|
|
410
|
-
generation: string,
|
|
411
|
-
): TerminalReplicaUpdate<string, string> {
|
|
412
|
-
return { ...terminalSeed(generation), semanticPaneId };
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
describe("WorkspaceClient", () => {
|
|
416
|
-
it("publishes a bounded degraded state when semantic shell projection rejects", async () => {
|
|
417
|
-
const valid = shellResource("alpha");
|
|
418
|
-
const corrupt = {
|
|
419
|
-
...valid,
|
|
420
|
-
workspace: {
|
|
421
|
-
...valid.workspace,
|
|
422
|
-
sidebar: {
|
|
423
|
-
...valid.workspace.sidebar,
|
|
424
|
-
agents: valid.workspace.sidebar.agents.map((agent, index) =>
|
|
425
|
-
index < 2 ? { ...agent, paneId: "pane.alpha" } : agent,
|
|
426
|
-
),
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
} as ApplicationShellProjectionInputV3;
|
|
430
|
-
const shell = shellBroker({ alpha: corrupt });
|
|
431
|
-
const client = createWorkspaceClient({
|
|
432
|
-
target: target("alpha"),
|
|
433
|
-
ports: {
|
|
434
|
-
shell: shell.transport,
|
|
435
|
-
connectRuntime: async () => new FakeRuntime(ALPHA_DAEMON.instanceId),
|
|
436
|
-
actions,
|
|
437
|
-
},
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
await settle();
|
|
441
|
-
expect(client.getSnapshot().shell).toMatchObject({
|
|
442
|
-
status: "degraded",
|
|
443
|
-
code: "schema-invalid",
|
|
444
|
-
data: null,
|
|
445
|
-
reason: "The application shell failed semantic projection.",
|
|
446
|
-
});
|
|
447
|
-
expect(client.getSnapshot().semantic).toBeNull();
|
|
448
|
-
await client.dispose();
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it("defers application-shell until terminal authority adopts through runtime staging", async () => {
|
|
452
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
453
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
454
|
-
const inventories: WorkspaceClientRuntimeInventory[] = [];
|
|
455
|
-
const client = createWorkspaceClient({
|
|
456
|
-
target: target("alpha"),
|
|
457
|
-
deferApplicationShell: true,
|
|
458
|
-
ports: {
|
|
459
|
-
shell: shell.transport,
|
|
460
|
-
connectRuntime: async (_current, inventory) => {
|
|
461
|
-
inventories.push(inventory);
|
|
462
|
-
return runtime;
|
|
463
|
-
},
|
|
464
|
-
actions,
|
|
465
|
-
},
|
|
466
|
-
});
|
|
467
|
-
await settle();
|
|
468
|
-
expect(shell.connections).toHaveLength(0);
|
|
469
|
-
expect(inventories).toHaveLength(0);
|
|
470
|
-
|
|
471
|
-
expect(
|
|
472
|
-
client.adoptTerminalRuntimeInventory({
|
|
473
|
-
workspaceName: "alpha",
|
|
474
|
-
workspaceId: "workspace.alpha",
|
|
475
|
-
sessionId: COHESION_FIXTURE_V1.workspace.session.id,
|
|
476
|
-
resourceRevision: 7,
|
|
477
|
-
semanticPaneIds: ["pane.alpha"],
|
|
478
|
-
}),
|
|
479
|
-
).toBe(true);
|
|
480
|
-
await settle();
|
|
481
|
-
expect(inventories).toHaveLength(1);
|
|
482
|
-
expect(inventories[0]).toMatchObject({
|
|
483
|
-
terminalResourceRevision: 7,
|
|
484
|
-
shellGeneration: 1,
|
|
485
|
-
semanticPaneIds: ["pane.alpha"],
|
|
486
|
-
});
|
|
487
|
-
expect(shell.connections).toHaveLength(1);
|
|
488
|
-
await client.dispose();
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
it("never lets late V2 topology roll back a newer terminal revision", async () => {
|
|
492
|
-
const lateV2 = deferred<ApplicationShellProjectionInputV3>();
|
|
493
|
-
const shellConnections: ShellConnection[] = [];
|
|
494
|
-
const shellTransport: ApplicationShellTransport<ApplicationShellProjectionInputV3> = {
|
|
495
|
-
validateTarget: (value) => DesktopApplicationShellTargetSchemaZ.parse(value),
|
|
496
|
-
fetchApplicationShell: () => lateV2.promise,
|
|
497
|
-
connectEvents: (current, handlers) => {
|
|
498
|
-
shellConnections.push({ target: current, handlers, closed: false });
|
|
499
|
-
return { close: () => undefined };
|
|
500
|
-
},
|
|
501
|
-
};
|
|
502
|
-
const inventories: WorkspaceClientRuntimeInventory[] = [];
|
|
503
|
-
const client = createWorkspaceClient({
|
|
504
|
-
target: target("alpha"),
|
|
505
|
-
deferApplicationShell: true,
|
|
506
|
-
ports: {
|
|
507
|
-
shell: shellTransport,
|
|
508
|
-
connectRuntime: async (_current, inventory) => {
|
|
509
|
-
inventories.push(inventory);
|
|
510
|
-
return new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
511
|
-
},
|
|
512
|
-
actions,
|
|
513
|
-
},
|
|
514
|
-
});
|
|
515
|
-
const base = {
|
|
516
|
-
workspaceName: "alpha",
|
|
517
|
-
workspaceId: "workspace.alpha",
|
|
518
|
-
sessionId: COHESION_FIXTURE_V1.workspace.session.id,
|
|
519
|
-
};
|
|
520
|
-
client.adoptTerminalRuntimeInventory({
|
|
521
|
-
...base,
|
|
522
|
-
resourceRevision: 1,
|
|
523
|
-
semanticPaneIds: ["pane.a"],
|
|
524
|
-
});
|
|
525
|
-
client.adoptTerminalRuntimeInventory({
|
|
526
|
-
...base,
|
|
527
|
-
resourceRevision: 2,
|
|
528
|
-
semanticPaneIds: ["pane.b"],
|
|
529
|
-
});
|
|
530
|
-
lateV2.resolve(shellResource("alpha", ["pane.a"]));
|
|
531
|
-
await settle();
|
|
532
|
-
// The obsolete first inventory is coalesced before physical connection.
|
|
533
|
-
expect(inventories.map((inventory) => inventory.semanticPaneIds)).toEqual([["pane.b"]]);
|
|
534
|
-
expect(
|
|
535
|
-
client.adoptTerminalRuntimeInventory({
|
|
536
|
-
...base,
|
|
537
|
-
resourceRevision: 1,
|
|
538
|
-
semanticPaneIds: ["pane.a"],
|
|
539
|
-
}),
|
|
540
|
-
).toBe(false);
|
|
541
|
-
await client.dispose();
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
it("treats V2 mismatch as a terminal-authority refresh request only", async () => {
|
|
545
|
-
const shell = shellBroker({ alpha: shellResource("alpha", ["pane.b"]) });
|
|
546
|
-
let refreshes = 0;
|
|
547
|
-
const inventories: WorkspaceClientRuntimeInventory[] = [];
|
|
548
|
-
const client = createWorkspaceClient({
|
|
549
|
-
target: target("alpha"),
|
|
550
|
-
deferApplicationShell: true,
|
|
551
|
-
ports: {
|
|
552
|
-
shell: shell.transport,
|
|
553
|
-
connectRuntime: async (_current, inventory) => {
|
|
554
|
-
inventories.push(inventory);
|
|
555
|
-
return new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
556
|
-
},
|
|
557
|
-
requestTerminalRuntimeInventoryRefresh: () => {
|
|
558
|
-
refreshes += 1;
|
|
559
|
-
},
|
|
560
|
-
actions,
|
|
561
|
-
},
|
|
562
|
-
});
|
|
563
|
-
client.adoptTerminalRuntimeInventory({
|
|
564
|
-
workspaceName: "alpha",
|
|
565
|
-
workspaceId: "workspace.alpha",
|
|
566
|
-
sessionId: COHESION_FIXTURE_V1.workspace.session.id,
|
|
567
|
-
resourceRevision: 1,
|
|
568
|
-
semanticPaneIds: ["pane.a"],
|
|
569
|
-
});
|
|
570
|
-
await settle();
|
|
571
|
-
expect(inventories.map((inventory) => inventory.semanticPaneIds)).toEqual([["pane.a"]]);
|
|
572
|
-
expect(refreshes).toBe(1);
|
|
573
|
-
for (let index = 0; index < 2_000; index += 1) {
|
|
574
|
-
expect(
|
|
575
|
-
client.adoptTerminalRuntimeInventory({
|
|
576
|
-
workspaceName: "alpha",
|
|
577
|
-
workspaceId: "workspace.alpha",
|
|
578
|
-
sessionId: COHESION_FIXTURE_V1.workspace.session.id,
|
|
579
|
-
resourceRevision: 1,
|
|
580
|
-
semanticPaneIds: ["pane.a"],
|
|
581
|
-
}),
|
|
582
|
-
).toBe(true);
|
|
583
|
-
}
|
|
584
|
-
await settle();
|
|
585
|
-
expect(refreshes).toBe(1);
|
|
586
|
-
|
|
587
|
-
shell.byWorkspace.set("alpha", shellResource("alpha", ["pane.c"]));
|
|
588
|
-
shell.connections.at(-1)?.handlers.onInvalidate();
|
|
589
|
-
await settle();
|
|
590
|
-
expect(refreshes).toBe(2);
|
|
591
|
-
|
|
592
|
-
expect(
|
|
593
|
-
client.adoptTerminalRuntimeInventory({
|
|
594
|
-
workspaceName: "alpha",
|
|
595
|
-
workspaceId: "workspace.alpha",
|
|
596
|
-
sessionId: COHESION_FIXTURE_V1.workspace.session.id,
|
|
597
|
-
resourceRevision: 2,
|
|
598
|
-
semanticPaneIds: ["pane.c"],
|
|
599
|
-
}),
|
|
600
|
-
).toBe(true);
|
|
601
|
-
await settle();
|
|
602
|
-
expect(refreshes).toBe(2);
|
|
603
|
-
await client.dispose();
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
it("bounds stale-topology refresh and reopens only the replacement inventory", async () => {
|
|
607
|
-
const shell = shellBroker({ alpha: shellResource("alpha", ["pane.a"]) });
|
|
608
|
-
const inventories: string[][] = [];
|
|
609
|
-
let refreshes = 0;
|
|
610
|
-
let client!: ReturnType<typeof createWorkspaceClient>;
|
|
611
|
-
client = createWorkspaceClient({
|
|
612
|
-
target: target("alpha"),
|
|
613
|
-
deferApplicationShell: true,
|
|
614
|
-
ports: {
|
|
615
|
-
shell: shell.transport,
|
|
616
|
-
connectRuntime: async (_current, inventory) => {
|
|
617
|
-
inventories.push([...inventory.semanticPaneIds]);
|
|
618
|
-
if (inventories.length === 1)
|
|
619
|
-
throw Object.assign(new Error("stale terminal topology"), {
|
|
620
|
-
code: "topology-changed",
|
|
621
|
-
});
|
|
622
|
-
return new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
623
|
-
},
|
|
624
|
-
requestTerminalRuntimeInventoryRefresh: () => {
|
|
625
|
-
refreshes += 1;
|
|
626
|
-
shell.byWorkspace.set("alpha", shellResource("alpha", ["pane.b"]));
|
|
627
|
-
queueMicrotask(() => {
|
|
628
|
-
client.adoptTerminalRuntimeInventory({
|
|
629
|
-
workspaceName: "alpha",
|
|
630
|
-
workspaceId: "workspace.alpha",
|
|
631
|
-
sessionId: COHESION_FIXTURE_V1.workspace.session.id,
|
|
632
|
-
resourceRevision: 2,
|
|
633
|
-
semanticPaneIds: ["pane.b"],
|
|
634
|
-
});
|
|
635
|
-
});
|
|
636
|
-
},
|
|
637
|
-
actions,
|
|
638
|
-
},
|
|
639
|
-
});
|
|
640
|
-
client.adoptTerminalRuntimeInventory({
|
|
641
|
-
workspaceName: "alpha",
|
|
642
|
-
workspaceId: "workspace.alpha",
|
|
643
|
-
sessionId: COHESION_FIXTURE_V1.workspace.session.id,
|
|
644
|
-
resourceRevision: 1,
|
|
645
|
-
semanticPaneIds: ["pane.a"],
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
await settle();
|
|
649
|
-
await settle();
|
|
650
|
-
expect(refreshes).toBe(2);
|
|
651
|
-
expect(inventories).toEqual([["pane.a"], ["pane.b"]]);
|
|
652
|
-
await client.dispose();
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
it("refreshes stale topology when an active runtime reconnects after ordinary tmux changes", async () => {
|
|
656
|
-
const shell = shellBroker({ alpha: shellResource("alpha", ["pane.a"]) });
|
|
657
|
-
const clock = new FakeClock();
|
|
658
|
-
const firstRuntime = new ClosableFakeRuntime(ALPHA_DAEMON.instanceId);
|
|
659
|
-
const replacementRuntime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
660
|
-
const inventories: string[][] = [];
|
|
661
|
-
const activatedInventories: string[][] = [];
|
|
662
|
-
let refreshes = 0;
|
|
663
|
-
let client!: ReturnType<typeof createWorkspaceClient>;
|
|
664
|
-
client = createWorkspaceClient({
|
|
665
|
-
target: target("alpha"),
|
|
666
|
-
deferApplicationShell: true,
|
|
667
|
-
clock,
|
|
668
|
-
ports: {
|
|
669
|
-
shell: shell.transport,
|
|
670
|
-
connectRuntime: async (_current, inventory) => {
|
|
671
|
-
inventories.push([...inventory.semanticPaneIds]);
|
|
672
|
-
if (inventories.length === 1) return firstRuntime;
|
|
673
|
-
if (inventory.semanticPaneIds.includes("pane.a")) {
|
|
674
|
-
throw Object.assign(new Error("ordinary tmux changed the pane topology"), {
|
|
675
|
-
code: "topology-changed",
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
return replacementRuntime;
|
|
679
|
-
},
|
|
680
|
-
requestTerminalRuntimeInventoryRefresh: () => {
|
|
681
|
-
refreshes += 1;
|
|
682
|
-
if (refreshes !== 1) return;
|
|
683
|
-
shell.byWorkspace.set("alpha", shellResource("alpha", ["pane.b"]));
|
|
684
|
-
shell.connections.at(-1)?.handlers.onInvalidate();
|
|
685
|
-
queueMicrotask(() => {
|
|
686
|
-
client.adoptTerminalRuntimeInventory({
|
|
687
|
-
workspaceName: "alpha",
|
|
688
|
-
workspaceId: "workspace.alpha",
|
|
689
|
-
sessionId: COHESION_FIXTURE_V1.workspace.session.id,
|
|
690
|
-
resourceRevision: 2,
|
|
691
|
-
semanticPaneIds: ["pane.b"],
|
|
692
|
-
});
|
|
693
|
-
});
|
|
694
|
-
},
|
|
695
|
-
didActivateRuntime: (_runtime, inventory) => {
|
|
696
|
-
activatedInventories.push([...inventory.semanticPaneIds]);
|
|
697
|
-
},
|
|
698
|
-
actions,
|
|
699
|
-
},
|
|
700
|
-
});
|
|
701
|
-
client.adoptTerminalRuntimeInventory({
|
|
702
|
-
workspaceName: "alpha",
|
|
703
|
-
workspaceId: "workspace.alpha",
|
|
704
|
-
sessionId: COHESION_FIXTURE_V1.workspace.session.id,
|
|
705
|
-
resourceRevision: 1,
|
|
706
|
-
semanticPaneIds: ["pane.a"],
|
|
707
|
-
});
|
|
708
|
-
await settle();
|
|
709
|
-
expect(inventories).toEqual([["pane.a"]]);
|
|
710
|
-
|
|
711
|
-
firstRuntime.fail();
|
|
712
|
-
await settle();
|
|
713
|
-
clock.advance(1_000);
|
|
714
|
-
await settle();
|
|
715
|
-
await settle();
|
|
716
|
-
|
|
717
|
-
expect(inventories).toEqual([["pane.a"], ["pane.a"], ["pane.b"]]);
|
|
718
|
-
expect(refreshes).toBe(2);
|
|
719
|
-
expect(activatedInventories).toEqual([["pane.a"], ["pane.b"]]);
|
|
720
|
-
await client.dispose();
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
it("fails closed when one terminal revision carries conflicting authority", async () => {
|
|
724
|
-
const shell = shellBroker({ alpha: shellResource("alpha", ["pane.a"]) });
|
|
725
|
-
const inventories: WorkspaceClientRuntimeInventory[] = [];
|
|
726
|
-
let refreshes = 0;
|
|
727
|
-
const client = createWorkspaceClient({
|
|
728
|
-
target: target("alpha"),
|
|
729
|
-
deferApplicationShell: true,
|
|
730
|
-
ports: {
|
|
731
|
-
shell: shell.transport,
|
|
732
|
-
connectRuntime: async (_current, inventory) => {
|
|
733
|
-
inventories.push(inventory);
|
|
734
|
-
return new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
735
|
-
},
|
|
736
|
-
requestTerminalRuntimeInventoryRefresh: () => {
|
|
737
|
-
refreshes += 1;
|
|
738
|
-
},
|
|
739
|
-
actions,
|
|
740
|
-
},
|
|
741
|
-
});
|
|
742
|
-
const base = {
|
|
743
|
-
workspaceName: "alpha",
|
|
744
|
-
workspaceId: "workspace.alpha",
|
|
745
|
-
sessionId: COHESION_FIXTURE_V1.workspace.session.id,
|
|
746
|
-
resourceRevision: 7,
|
|
747
|
-
};
|
|
748
|
-
expect(client.adoptTerminalRuntimeInventory({ ...base, semanticPaneIds: ["pane.a"] })).toBe(
|
|
749
|
-
true,
|
|
750
|
-
);
|
|
751
|
-
await settle();
|
|
752
|
-
expect(client.adoptTerminalRuntimeInventory({ ...base, semanticPaneIds: ["pane.b"] })).toBe(
|
|
753
|
-
false,
|
|
754
|
-
);
|
|
755
|
-
await settle();
|
|
756
|
-
expect(inventories.map((inventory) => inventory.semanticPaneIds)).toEqual([["pane.a"]]);
|
|
757
|
-
expect(refreshes).toBe(1);
|
|
758
|
-
expect(client.adoptTerminalRuntimeInventory({ ...base, semanticPaneIds: ["pane.b"] })).toBe(
|
|
759
|
-
false,
|
|
760
|
-
);
|
|
761
|
-
expect(refreshes).toBe(1);
|
|
762
|
-
await client.dispose();
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
it("measures a burst of topology replacements without connecting obsolete inventories", async () => {
|
|
766
|
-
const shell = shellBroker({ alpha: shellResource("alpha", []) });
|
|
767
|
-
const inventories: WorkspaceClientRuntimeInventory[] = [];
|
|
768
|
-
const client = createWorkspaceClient({
|
|
769
|
-
target: target("alpha"),
|
|
770
|
-
deferApplicationShell: true,
|
|
771
|
-
ports: {
|
|
772
|
-
shell: shell.transport,
|
|
773
|
-
actions,
|
|
774
|
-
connectRuntime: async (_current, inventory) => {
|
|
775
|
-
inventories.push(inventory);
|
|
776
|
-
return new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
777
|
-
},
|
|
778
|
-
},
|
|
779
|
-
});
|
|
780
|
-
const start = performance.now();
|
|
781
|
-
for (let revision = 1; revision <= 8; revision++)
|
|
782
|
-
client.adoptTerminalRuntimeInventory({
|
|
783
|
-
workspaceName: "alpha",
|
|
784
|
-
workspaceId: "workspace.alpha",
|
|
785
|
-
sessionId: COHESION_FIXTURE_V1.workspace.session.id,
|
|
786
|
-
resourceRevision: revision,
|
|
787
|
-
semanticPaneIds: Array.from({ length: revision }, (_, i) => `pane.${i}`),
|
|
788
|
-
});
|
|
789
|
-
await settle();
|
|
790
|
-
console.log(
|
|
791
|
-
"REBIND_BURST",
|
|
792
|
-
JSON.stringify({
|
|
793
|
-
revisions: 8,
|
|
794
|
-
connections: inventories.length,
|
|
795
|
-
elapsedMs: performance.now() - start,
|
|
796
|
-
}),
|
|
797
|
-
);
|
|
798
|
-
expect(inventories.at(-1)?.semanticPaneIds).toHaveLength(8);
|
|
799
|
-
expect(inventories).toHaveLength(1);
|
|
800
|
-
await client.dispose();
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
it("treats empty inventory and same-name session replacement as terminal revisions", async () => {
|
|
804
|
-
const shell = shellBroker({ alpha: shellResource("alpha", []) });
|
|
805
|
-
const inventories: WorkspaceClientRuntimeInventory[] = [];
|
|
806
|
-
let retirements = 0;
|
|
807
|
-
const client = createWorkspaceClient({
|
|
808
|
-
target: target("alpha"),
|
|
809
|
-
deferApplicationShell: true,
|
|
810
|
-
ports: {
|
|
811
|
-
shell: shell.transport,
|
|
812
|
-
connectRuntime: async (_current, inventory) => {
|
|
813
|
-
inventories.push(inventory);
|
|
814
|
-
return new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
815
|
-
},
|
|
816
|
-
didRetireRuntime: () => {
|
|
817
|
-
retirements += 1;
|
|
818
|
-
},
|
|
819
|
-
actions,
|
|
820
|
-
},
|
|
821
|
-
});
|
|
822
|
-
const base = { workspaceName: "alpha", workspaceId: "workspace.alpha" };
|
|
823
|
-
client.adoptTerminalRuntimeInventory({
|
|
824
|
-
...base,
|
|
825
|
-
sessionId: "session.aaaaaaaaaaaaaaaaaaaa",
|
|
826
|
-
resourceRevision: 1,
|
|
827
|
-
semanticPaneIds: ["pane.a"],
|
|
828
|
-
});
|
|
829
|
-
await settle();
|
|
830
|
-
client.adoptTerminalRuntimeInventory({
|
|
831
|
-
...base,
|
|
832
|
-
sessionId: "session.bbbbbbbbbbbbbbbbbbbb",
|
|
833
|
-
resourceRevision: 2,
|
|
834
|
-
semanticPaneIds: ["pane.a"],
|
|
835
|
-
});
|
|
836
|
-
await settle();
|
|
837
|
-
expect(inventories.map(({ sessionId }) => sessionId)).toEqual([
|
|
838
|
-
"session.aaaaaaaaaaaaaaaaaaaa",
|
|
839
|
-
"session.bbbbbbbbbbbbbbbbbbbb",
|
|
840
|
-
]);
|
|
841
|
-
client.adoptTerminalRuntimeInventory({
|
|
842
|
-
...base,
|
|
843
|
-
sessionId: "session.bbbbbbbbbbbbbbbbbbbb",
|
|
844
|
-
resourceRevision: 3,
|
|
845
|
-
semanticPaneIds: [],
|
|
846
|
-
});
|
|
847
|
-
await settle();
|
|
848
|
-
expect(retirements).toBeGreaterThan(0);
|
|
849
|
-
await client.dispose();
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
it("waits for shell authority and connects with its exact immutable terminal inventory", async () => {
|
|
853
|
-
const shellAuthority = deferred<ApplicationShellProjectionInputV3>();
|
|
854
|
-
const shellConnections: ShellConnection[] = [];
|
|
855
|
-
const shellTransport: ApplicationShellTransport<ApplicationShellProjectionInputV3> = {
|
|
856
|
-
validateTarget: (value) => DesktopApplicationShellTargetSchemaZ.parse(value),
|
|
857
|
-
fetchApplicationShell: async () => shellAuthority.promise,
|
|
858
|
-
connectEvents: (current, handlers) => {
|
|
859
|
-
const connection = { target: current, handlers, closed: false };
|
|
860
|
-
shellConnections.push(connection);
|
|
861
|
-
return { close: () => (connection.closed = true) };
|
|
862
|
-
},
|
|
863
|
-
};
|
|
864
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
865
|
-
const inventories: WorkspaceClientRuntimeInventory[] = [];
|
|
866
|
-
const client = createWorkspaceClient({
|
|
867
|
-
target: target("alpha"),
|
|
868
|
-
ports: {
|
|
869
|
-
shell: shellTransport,
|
|
870
|
-
connectRuntime: async (_current, inventory) => {
|
|
871
|
-
inventories.push(inventory);
|
|
872
|
-
return runtime;
|
|
873
|
-
},
|
|
874
|
-
actions,
|
|
875
|
-
},
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
await settle();
|
|
879
|
-
expect(inventories).toHaveLength(0);
|
|
880
|
-
shellAuthority.resolve(shellResource("alpha", ["pane.z", "pane.a"]));
|
|
881
|
-
await settle();
|
|
882
|
-
|
|
883
|
-
expect(inventories).toHaveLength(1);
|
|
884
|
-
expect(inventories[0]).toEqual({
|
|
885
|
-
workspaceName: "alpha",
|
|
886
|
-
workspaceId: "workspace.alpha",
|
|
887
|
-
sessionId: COHESION_FIXTURE_V1.workspace.session.id,
|
|
888
|
-
daemonGeneration: ALPHA_DAEMON.instanceId,
|
|
889
|
-
shellGeneration: 1,
|
|
890
|
-
semanticPaneIds: ["pane.a", "pane.z"],
|
|
891
|
-
});
|
|
892
|
-
expect(Object.isFrozen(inventories[0])).toBe(true);
|
|
893
|
-
expect(Object.isFrozen(inventories[0]!.semanticPaneIds)).toBe(true);
|
|
894
|
-
client.dispose();
|
|
895
|
-
await settle();
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
it("primes every inventory pane before coherence and adopts the retained candidate bindings", async () => {
|
|
899
|
-
const paneIds = ["pane.window-a.one", "pane.window-a.two", "pane.window-b.hidden"];
|
|
900
|
-
const shell = shellBroker({ alpha: shellResource("alpha", paneIds) });
|
|
901
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
902
|
-
const coherent = deferred<void>();
|
|
903
|
-
const updates = new Map<string, TerminalReplicaUpdate<string, string>[]>();
|
|
904
|
-
let client!: WorkspaceClient<string, string>;
|
|
905
|
-
let prepared = false;
|
|
906
|
-
client = createWorkspaceClient<string, string>({
|
|
907
|
-
target: target("alpha"),
|
|
908
|
-
ports: {
|
|
909
|
-
shell: shell.transport,
|
|
910
|
-
connectRuntime: async (_current, _runtimeInventory, _signal, prepare) => {
|
|
911
|
-
await prepare(runtime);
|
|
912
|
-
prepared = true;
|
|
913
|
-
await coherent.promise;
|
|
914
|
-
return runtime;
|
|
915
|
-
},
|
|
916
|
-
actions,
|
|
917
|
-
},
|
|
918
|
-
});
|
|
919
|
-
const unsubscribes = paneIds.map((semanticPaneId) =>
|
|
920
|
-
client.subscribeTerminal({ workspaceName: "alpha", semanticPaneId }, (update) => {
|
|
921
|
-
const seen = updates.get(semanticPaneId) ?? [];
|
|
922
|
-
seen.push(update);
|
|
923
|
-
updates.set(semanticPaneId, seen);
|
|
924
|
-
}),
|
|
925
|
-
);
|
|
926
|
-
|
|
927
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
928
|
-
await settle();
|
|
929
|
-
expect(prepared).toBe(true);
|
|
930
|
-
expect([...runtime.subscriptions.keys()].sort()).toEqual(paneIds);
|
|
931
|
-
|
|
932
|
-
for (const semanticPaneId of paneIds) {
|
|
933
|
-
runtime.subscriptions
|
|
934
|
-
.get(semanticPaneId)!
|
|
935
|
-
.emit(terminalSeedFor(semanticPaneId, ALPHA_DAEMON.instanceId));
|
|
936
|
-
}
|
|
937
|
-
expect(updates.get("pane.window-b.hidden")).toBeUndefined();
|
|
938
|
-
|
|
939
|
-
coherent.resolve();
|
|
940
|
-
await settle();
|
|
941
|
-
expect(updates.get("pane.window-b.hidden")).toHaveLength(1);
|
|
942
|
-
expect(client.getSnapshot().authorityShell).not.toBeNull();
|
|
943
|
-
expect([...runtime.subscriptions.keys()].sort()).toEqual(paneIds);
|
|
944
|
-
expect(
|
|
945
|
-
[...runtime.subscriptions.values()].every((subscription) => subscription.closeCount === 0),
|
|
946
|
-
).toBe(true);
|
|
947
|
-
|
|
948
|
-
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
949
|
-
client.dispose();
|
|
950
|
-
await settle();
|
|
951
|
-
expect(
|
|
952
|
-
[...runtime.subscriptions.values()].every((subscription) => subscription.closeCount === 1),
|
|
953
|
-
).toBe(true);
|
|
954
|
-
});
|
|
955
|
-
|
|
956
|
-
it("does not charge an eager canonical seed against the candidate update buffer", async () => {
|
|
957
|
-
const shell = shellBroker({ alpha: shellResource("alpha", ["pane.alpha"]) });
|
|
958
|
-
const runtime = new EagerSeedRuntime(ALPHA_DAEMON.instanceId, "x".repeat(8 * 1024 * 1024 + 1));
|
|
959
|
-
const publications: TerminalReplicaUpdate<string, string>[] = [];
|
|
960
|
-
const activations: FakeRuntime[] = [];
|
|
961
|
-
const client = createWorkspaceClient<string, string>({
|
|
962
|
-
target: target("alpha"),
|
|
963
|
-
ports: {
|
|
964
|
-
shell: shell.transport,
|
|
965
|
-
connectRuntime: async (_current, _inventory, _signal, prepare) => {
|
|
966
|
-
await prepare(runtime);
|
|
967
|
-
return runtime;
|
|
968
|
-
},
|
|
969
|
-
didActivateRuntime: (nextRuntime) => activations.push(nextRuntime as FakeRuntime),
|
|
970
|
-
actions,
|
|
971
|
-
},
|
|
972
|
-
});
|
|
973
|
-
client.subscribeTerminal({ workspaceName: "alpha", semanticPaneId: "pane.alpha" }, (update) =>
|
|
974
|
-
publications.push(update),
|
|
975
|
-
);
|
|
976
|
-
|
|
977
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
978
|
-
await settle();
|
|
979
|
-
|
|
980
|
-
expect(activations).toEqual([runtime]);
|
|
981
|
-
expect(publications).toHaveLength(1);
|
|
982
|
-
expect(publications[0]?.type).toBe("terminal.seed");
|
|
983
|
-
await client.dispose();
|
|
984
|
-
});
|
|
985
|
-
|
|
986
|
-
it("does not reconnect when a shell refresh leaves runtime inventory unchanged", async () => {
|
|
987
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
988
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
989
|
-
let connects = 0;
|
|
990
|
-
const client = createWorkspaceClient({
|
|
991
|
-
target: target("alpha"),
|
|
992
|
-
ports: {
|
|
993
|
-
shell: shell.transport,
|
|
994
|
-
connectRuntime: async () => {
|
|
995
|
-
connects += 1;
|
|
996
|
-
return runtime;
|
|
997
|
-
},
|
|
998
|
-
actions,
|
|
999
|
-
},
|
|
1000
|
-
});
|
|
1001
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1002
|
-
await settle();
|
|
1003
|
-
expect(connects).toBe(1);
|
|
1004
|
-
|
|
1005
|
-
shell.byWorkspace.set(
|
|
1006
|
-
"alpha",
|
|
1007
|
-
ApplicationShellProjectionInputV3SchemaZ.parse({
|
|
1008
|
-
...shellResource("alpha"),
|
|
1009
|
-
project: { ...shellResource("alpha").project, rootLabel: "refreshed label" },
|
|
1010
|
-
}),
|
|
1011
|
-
);
|
|
1012
|
-
shell.connections[0]!.handlers.onInvalidate();
|
|
1013
|
-
await settle();
|
|
1014
|
-
expect(connects).toBe(1);
|
|
1015
|
-
expect(runtime.closeCount).toBe(0);
|
|
1016
|
-
client.dispose();
|
|
1017
|
-
await settle();
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
it("prepares a changed pane inventory and atomically swaps only after it is live", async () => {
|
|
1021
|
-
const shell = shellBroker({ alpha: shellResource("alpha", ["pane.alpha"]) });
|
|
1022
|
-
const first = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1023
|
-
const second = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1024
|
-
const next = deferred<FakeRuntime>();
|
|
1025
|
-
const calls: WorkspaceClientRuntimeInventory[] = [];
|
|
1026
|
-
const activated: FakeRuntime[] = [];
|
|
1027
|
-
const activatedInventories: string[][] = [];
|
|
1028
|
-
const client = createWorkspaceClient({
|
|
1029
|
-
target: target("alpha"),
|
|
1030
|
-
ports: {
|
|
1031
|
-
shell: shell.transport,
|
|
1032
|
-
connectRuntime: async (_current, inventory) => {
|
|
1033
|
-
calls.push(inventory);
|
|
1034
|
-
return calls.length === 1 ? first : next.promise;
|
|
1035
|
-
},
|
|
1036
|
-
didActivateRuntime: (runtime, inventory) => {
|
|
1037
|
-
activated.push(runtime as FakeRuntime);
|
|
1038
|
-
activatedInventories.push([...inventory.semanticPaneIds]);
|
|
1039
|
-
},
|
|
1040
|
-
actions,
|
|
1041
|
-
},
|
|
1042
|
-
});
|
|
1043
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1044
|
-
await settle();
|
|
1045
|
-
expect(calls).toHaveLength(1);
|
|
1046
|
-
expect(activated).toEqual([first]);
|
|
1047
|
-
|
|
1048
|
-
shell.byWorkspace.set("alpha", shellResource("alpha", ["pane.beta", "pane.alpha"]));
|
|
1049
|
-
shell.connections[0]!.handlers.onInvalidate();
|
|
1050
|
-
await settle();
|
|
1051
|
-
expect(calls).toHaveLength(2);
|
|
1052
|
-
expect(calls[1]!.semanticPaneIds).toEqual(["pane.alpha", "pane.beta"]);
|
|
1053
|
-
expect(first.closeCount).toBe(0);
|
|
1054
|
-
expect(activated).toEqual([first]);
|
|
1055
|
-
expect(activatedInventories).toEqual([["pane.alpha"]]);
|
|
1056
|
-
expect(
|
|
1057
|
-
await client.sendTerminalInput(
|
|
1058
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.alpha" },
|
|
1059
|
-
{ kind: "key", data: "Enter" },
|
|
1060
|
-
),
|
|
1061
|
-
).toBe("ok");
|
|
1062
|
-
expect(first.inputs).toHaveLength(1);
|
|
1063
|
-
|
|
1064
|
-
next.resolve(second);
|
|
1065
|
-
await settle();
|
|
1066
|
-
expect(activated).toEqual([first, second]);
|
|
1067
|
-
expect(activatedInventories).toEqual([["pane.alpha"], ["pane.alpha", "pane.beta"]]);
|
|
1068
|
-
expect(first.closeCount).toBe(1);
|
|
1069
|
-
expect(
|
|
1070
|
-
await client.sendTerminalInput(
|
|
1071
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.beta" },
|
|
1072
|
-
{ kind: "text", data: "ready" },
|
|
1073
|
-
),
|
|
1074
|
-
).toBe("ok");
|
|
1075
|
-
expect(second.inputs).toHaveLength(1);
|
|
1076
|
-
expect(calls).toHaveLength(2);
|
|
1077
|
-
client.dispose();
|
|
1078
|
-
await settle();
|
|
1079
|
-
});
|
|
1080
|
-
|
|
1081
|
-
it("does not reconnect a closed incumbent after a replacement inventory is desired", async () => {
|
|
1082
|
-
const shell = shellBroker({ alpha: shellResource("alpha", ["pane.alpha"]) });
|
|
1083
|
-
const clock = new FakeClock();
|
|
1084
|
-
const incumbent = new ClosableFakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1085
|
-
const replacement = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1086
|
-
const replacementGate = deferred<FakeRuntime>();
|
|
1087
|
-
const inventories: WorkspaceClientRuntimeInventory[] = [];
|
|
1088
|
-
const client = createWorkspaceClient({
|
|
1089
|
-
target: target("alpha"),
|
|
1090
|
-
clock,
|
|
1091
|
-
ports: {
|
|
1092
|
-
shell: shell.transport,
|
|
1093
|
-
connectRuntime: async (_current, inventory) => {
|
|
1094
|
-
inventories.push(inventory);
|
|
1095
|
-
return inventories.length === 1 ? incumbent : replacementGate.promise;
|
|
1096
|
-
},
|
|
1097
|
-
actions,
|
|
1098
|
-
},
|
|
1099
|
-
});
|
|
1100
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1101
|
-
await settle();
|
|
1102
|
-
expect(inventories).toHaveLength(1);
|
|
1103
|
-
|
|
1104
|
-
shell.byWorkspace.set("alpha", shellResource("alpha", ["pane.alpha", "pane.beta"]));
|
|
1105
|
-
shell.connections[0]!.handlers.onInvalidate();
|
|
1106
|
-
await settle();
|
|
1107
|
-
expect(inventories).toHaveLength(2);
|
|
1108
|
-
|
|
1109
|
-
incumbent.fail();
|
|
1110
|
-
await settle();
|
|
1111
|
-
clock.advance(30_000);
|
|
1112
|
-
await settle();
|
|
1113
|
-
expect(inventories).toHaveLength(2);
|
|
1114
|
-
expect(clock.timers.size).toBe(0);
|
|
1115
|
-
|
|
1116
|
-
replacementGate.resolve(replacement);
|
|
1117
|
-
await settle();
|
|
1118
|
-
await client.dispose();
|
|
1119
|
-
});
|
|
1120
|
-
|
|
1121
|
-
it("converges a rebound runtime select receipt with the invalidated active shell inventory", async () => {
|
|
1122
|
-
const shell = shellBroker({ alpha: shellResource("alpha", ["pane.alpha"]) });
|
|
1123
|
-
const first = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1124
|
-
const second = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1125
|
-
let connections = 0;
|
|
1126
|
-
const client = createWorkspaceClient({
|
|
1127
|
-
target: target("alpha"),
|
|
1128
|
-
ports: {
|
|
1129
|
-
shell: shell.transport,
|
|
1130
|
-
connectRuntime: async () => (++connections === 1 ? first : second),
|
|
1131
|
-
actions,
|
|
1132
|
-
},
|
|
1133
|
-
});
|
|
1134
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1135
|
-
await settle();
|
|
1136
|
-
shell.byWorkspace.set("alpha", shellResource("alpha", ["pane.alpha", "pane.beta"]));
|
|
1137
|
-
shell.connections[0]!.handlers.onInvalidate();
|
|
1138
|
-
await settle();
|
|
1139
|
-
expect(connections).toBe(2);
|
|
1140
|
-
|
|
1141
|
-
const operationId = "13000000-0000-4000-8000-000000000013";
|
|
1142
|
-
await client.dispatch({
|
|
1143
|
-
kind: "semantic-intent",
|
|
1144
|
-
operationId,
|
|
1145
|
-
intent: {
|
|
1146
|
-
verb: "workspace.pane.select",
|
|
1147
|
-
workspaceName: "alpha",
|
|
1148
|
-
semanticPaneId: "pane.beta",
|
|
1149
|
-
},
|
|
1150
|
-
});
|
|
1151
|
-
second.emitReceipt({
|
|
1152
|
-
...receipt(operationId, "observed"),
|
|
1153
|
-
target: { kind: "pane", semanticPaneId: "pane.beta" },
|
|
1154
|
-
operationKind: "workspace.pane.select",
|
|
1155
|
-
summary: { operationKind: "workspace.pane.select" },
|
|
1156
|
-
proof: {
|
|
1157
|
-
operationKind: "workspace.pane.select",
|
|
1158
|
-
outcome: "applied",
|
|
1159
|
-
semanticPaneId: "pane.beta",
|
|
1160
|
-
},
|
|
1161
|
-
});
|
|
1162
|
-
const externalRenameId = "14000000-0000-4000-8000-000000000014";
|
|
1163
|
-
shell.connections[0]!.handlers.onOperationAcknowledged?.({
|
|
1164
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
1165
|
-
operationId: externalRenameId,
|
|
1166
|
-
sequence: 10,
|
|
1167
|
-
revision: 8,
|
|
1168
|
-
});
|
|
1169
|
-
shell.byWorkspace.set("alpha", shellResource("alpha", ["pane.beta", "pane.alpha"]));
|
|
1170
|
-
shell.connections[0]!.handlers.onInvalidate();
|
|
1171
|
-
await settle();
|
|
1172
|
-
|
|
1173
|
-
const snapshot = client.getSnapshot();
|
|
1174
|
-
expect(snapshot.operations.pending).toEqual([]);
|
|
1175
|
-
expect(snapshot.operations.lastReceipt).toMatchObject({
|
|
1176
|
-
operationId,
|
|
1177
|
-
operationKind: "workspace.pane.select",
|
|
1178
|
-
phase: "observed",
|
|
1179
|
-
proof: { outcome: "applied", semanticPaneId: "pane.beta" },
|
|
1180
|
-
});
|
|
1181
|
-
expect(snapshot.operations.lastResourceChangeAcknowledgement).toEqual({
|
|
1182
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
1183
|
-
operationId: externalRenameId,
|
|
1184
|
-
sequence: 10,
|
|
1185
|
-
revision: 8,
|
|
1186
|
-
});
|
|
1187
|
-
expect(snapshot.authorityShell?.terminalInventory?.activeResourceId).toBe("pane.beta");
|
|
1188
|
-
expect(snapshot.semantic?.terminalInventory?.activeResourceId).toBe("pane.beta");
|
|
1189
|
-
expect(connections).toBe(2);
|
|
1190
|
-
|
|
1191
|
-
const acknowledgementAfterRefreshId = "15000000-0000-4000-8000-000000000015";
|
|
1192
|
-
const priorShell = shellResource("alpha", ["pane.beta", "pane.alpha"]);
|
|
1193
|
-
const renamedShell = ApplicationShellProjectionInputV3SchemaZ.parse({
|
|
1194
|
-
...priorShell,
|
|
1195
|
-
terminalInventory: {
|
|
1196
|
-
...priorShell.terminalInventory,
|
|
1197
|
-
resources: priorShell.terminalInventory.resources.map((resource, index) =>
|
|
1198
|
-
index === 0 ? { ...resource, title: "Lifecycle Renamed" } : resource,
|
|
1199
|
-
),
|
|
1200
|
-
},
|
|
1201
|
-
});
|
|
1202
|
-
shell.byWorkspace.set("alpha", renamedShell);
|
|
1203
|
-
shell.connections[0]!.handlers.onInvalidate();
|
|
1204
|
-
await settle();
|
|
1205
|
-
shell.connections[0]!.handlers.onOperationAcknowledged?.({
|
|
1206
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
1207
|
-
operationId: acknowledgementAfterRefreshId,
|
|
1208
|
-
sequence: 11,
|
|
1209
|
-
revision: 8,
|
|
1210
|
-
});
|
|
1211
|
-
expect(connections).toBe(2);
|
|
1212
|
-
expect(client.getSnapshot().semantic?.terminalInventory?.resources[0]?.title).toBe(
|
|
1213
|
-
"Lifecycle Renamed",
|
|
1214
|
-
);
|
|
1215
|
-
expect(client.getSnapshot().operations.lastResourceChangeAcknowledgement).toEqual({
|
|
1216
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
1217
|
-
operationId: acknowledgementAfterRefreshId,
|
|
1218
|
-
sequence: 11,
|
|
1219
|
-
revision: 8,
|
|
1220
|
-
});
|
|
1221
|
-
second.emitReceipt({
|
|
1222
|
-
...receipt(externalRenameId, "observed"),
|
|
1223
|
-
operationKind: "workspace.rename",
|
|
1224
|
-
summary: { operationKind: "workspace.rename", scope: "window" },
|
|
1225
|
-
target: { kind: "window", target: { by: "pane", semanticPaneId: "pane.beta" } },
|
|
1226
|
-
proof: { operationKind: "workspace.rename", outcome: "applied", scope: "window" },
|
|
1227
|
-
});
|
|
1228
|
-
expect(client.getSnapshot().operations.lastReceipt?.operationId).toBe(operationId);
|
|
1229
|
-
client.dispose();
|
|
1230
|
-
await settle();
|
|
1231
|
-
});
|
|
1232
|
-
|
|
1233
|
-
it("fences resource-change acknowledgements by generation and monotonic sequence", async () => {
|
|
1234
|
-
const shell = shellBroker({ alpha: shellResource("alpha"), beta: shellResource("beta") });
|
|
1235
|
-
const client = createWorkspaceClient({
|
|
1236
|
-
target: target("alpha"),
|
|
1237
|
-
ports: {
|
|
1238
|
-
shell: shell.transport,
|
|
1239
|
-
connectRuntime: async (current) => new FakeRuntime(current.daemon.instanceId),
|
|
1240
|
-
actions,
|
|
1241
|
-
},
|
|
1242
|
-
});
|
|
1243
|
-
const retiredHandlers = shell.connections[0]!.handlers;
|
|
1244
|
-
retiredHandlers.onVerifiedOpen();
|
|
1245
|
-
await settle();
|
|
1246
|
-
await client.setTarget(target("beta", ALPHA_DAEMON));
|
|
1247
|
-
const activeHandlers = shell.connections.findLast(
|
|
1248
|
-
(connection) => connection.target.workspaceName === "beta" && !connection.closed,
|
|
1249
|
-
)!.handlers;
|
|
1250
|
-
activeHandlers.onVerifiedOpen();
|
|
1251
|
-
await settle();
|
|
1252
|
-
|
|
1253
|
-
retiredHandlers.onOperationAcknowledged?.({
|
|
1254
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
1255
|
-
operationId: "16000000-0000-4000-8000-000000000016",
|
|
1256
|
-
sequence: 20,
|
|
1257
|
-
revision: 9,
|
|
1258
|
-
});
|
|
1259
|
-
expect(client.getSnapshot().operations.lastResourceChangeAcknowledgement).toBeNull();
|
|
1260
|
-
activeHandlers.onOperationAcknowledged?.({
|
|
1261
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
1262
|
-
operationId: "17000000-0000-4000-8000-000000000017",
|
|
1263
|
-
sequence: 21,
|
|
1264
|
-
revision: 10,
|
|
1265
|
-
});
|
|
1266
|
-
const accepted = client.getSnapshot().operations.lastResourceChangeAcknowledgement;
|
|
1267
|
-
expect(accepted).toEqual({
|
|
1268
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
1269
|
-
operationId: "17000000-0000-4000-8000-000000000017",
|
|
1270
|
-
sequence: 21,
|
|
1271
|
-
revision: 10,
|
|
1272
|
-
});
|
|
1273
|
-
for (const acknowledgement of [
|
|
1274
|
-
{
|
|
1275
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
1276
|
-
operationId: "18000000-0000-4000-8000-000000000018",
|
|
1277
|
-
sequence: 20,
|
|
1278
|
-
revision: 11,
|
|
1279
|
-
},
|
|
1280
|
-
{
|
|
1281
|
-
daemonInstanceId: BETA_DAEMON.instanceId,
|
|
1282
|
-
operationId: "18000000-0000-4000-8000-000000000018",
|
|
1283
|
-
sequence: 22,
|
|
1284
|
-
revision: 11,
|
|
1285
|
-
},
|
|
1286
|
-
{
|
|
1287
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
1288
|
-
operationId: "NOT-A-MINTED-OPERATION",
|
|
1289
|
-
sequence: 22,
|
|
1290
|
-
revision: 11,
|
|
1291
|
-
},
|
|
1292
|
-
])
|
|
1293
|
-
activeHandlers.onOperationAcknowledged?.(acknowledgement);
|
|
1294
|
-
expect(client.getSnapshot().operations.lastResourceChangeAcknowledgement).toBe(accepted);
|
|
1295
|
-
client.dispose();
|
|
1296
|
-
await settle();
|
|
1297
|
-
});
|
|
1298
|
-
|
|
1299
|
-
it("carries a supervised application-shell acknowledgement into WorkspaceClient before refresh", async () => {
|
|
1300
|
-
const socket = new WorkspaceClientEventSocket();
|
|
1301
|
-
const initialShell = shellResource("alpha", ["pane.alpha"]);
|
|
1302
|
-
let currentShell = ApplicationShellProjectionInputV3SchemaZ.parse({
|
|
1303
|
-
...initialShell,
|
|
1304
|
-
workspace: {
|
|
1305
|
-
...initialShell.workspace,
|
|
1306
|
-
id: "workspace.0123456789abcdefabcd",
|
|
1307
|
-
session: {
|
|
1308
|
-
...initialShell.workspace.session,
|
|
1309
|
-
id: "session.0123456789abcdefabcd",
|
|
1310
|
-
},
|
|
1311
|
-
},
|
|
1312
|
-
});
|
|
1313
|
-
const transport = createDirectLoopbackDaemonTransport({
|
|
1314
|
-
descriptor: { ...ALPHA_DAEMON, apiBaseUrl: "http://127.0.0.1:6060/" },
|
|
1315
|
-
resolveSessionName: () => "alpha",
|
|
1316
|
-
terminalRuntimeAuthority: true,
|
|
1317
|
-
applicationShellResourceVersion: APPLICATION_SHELL_RESOURCE_V3_VERSION,
|
|
1318
|
-
createWebSocket: () => socket,
|
|
1319
|
-
fetch: async (input) => {
|
|
1320
|
-
const url = new URL(String(input));
|
|
1321
|
-
if (url.pathname.endsWith("/terminal-runtime-inventory")) {
|
|
1322
|
-
return new Response(
|
|
1323
|
-
JSON.stringify({
|
|
1324
|
-
version: 1,
|
|
1325
|
-
daemon: ALPHA_DAEMON,
|
|
1326
|
-
resource: {
|
|
1327
|
-
workspaceName: "alpha",
|
|
1328
|
-
workspaceId: currentShell.workspace.id,
|
|
1329
|
-
sessionId: currentShell.workspace.session.id,
|
|
1330
|
-
resourceRevision: 7,
|
|
1331
|
-
semanticPaneIds: ["pane.alpha"],
|
|
1332
|
-
},
|
|
1333
|
-
}),
|
|
1334
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1335
|
-
);
|
|
1336
|
-
}
|
|
1337
|
-
return new Response(
|
|
1338
|
-
JSON.stringify({
|
|
1339
|
-
version: APPLICATION_SHELL_RESOURCE_V3_VERSION,
|
|
1340
|
-
daemon: ALPHA_DAEMON,
|
|
1341
|
-
resource: currentShell,
|
|
1342
|
-
}),
|
|
1343
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1344
|
-
);
|
|
1345
|
-
},
|
|
1346
|
-
}) as TerminalFirstDaemonTransport;
|
|
1347
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1348
|
-
const client = createWorkspaceClient({
|
|
1349
|
-
target: target("alpha"),
|
|
1350
|
-
deferApplicationShell: true,
|
|
1351
|
-
ports: { shell: transport, connectRuntime: async () => runtime, actions },
|
|
1352
|
-
});
|
|
1353
|
-
const preparation = transport.prepareTerminalRuntimeInventory(
|
|
1354
|
-
target("alpha"),
|
|
1355
|
-
new AbortController().signal,
|
|
1356
|
-
);
|
|
1357
|
-
socket.emit("open");
|
|
1358
|
-
socket.frame({ type: "hello", daemon: ALPHA_DAEMON, sessions: [], eventSequence: 0 });
|
|
1359
|
-
await settle();
|
|
1360
|
-
socket.frame({
|
|
1361
|
-
type: "resource.interests-ack",
|
|
1362
|
-
interestRevision: 1,
|
|
1363
|
-
sequence: 0,
|
|
1364
|
-
unavailableInterests: [],
|
|
1365
|
-
});
|
|
1366
|
-
const prepared = await preparation;
|
|
1367
|
-
const terminalAuthority = transport.adoptTerminalRuntimeInventory(prepared, (resource) => {
|
|
1368
|
-
client.adoptTerminalRuntimeInventory(resource);
|
|
1369
|
-
});
|
|
1370
|
-
expect(terminalAuthority).not.toBeNull();
|
|
1371
|
-
client.adoptTerminalRuntimeInventory(terminalAuthority!);
|
|
1372
|
-
await settle();
|
|
1373
|
-
expect(socket.sent[1]).toMatchObject({
|
|
1374
|
-
type: "subscribe",
|
|
1375
|
-
interestRevision: 2,
|
|
1376
|
-
interests: [{ resource: "terminal-runtime-inventory" }, { resource: "application-shell" }],
|
|
1377
|
-
});
|
|
1378
|
-
socket.frame({
|
|
1379
|
-
type: "resource.interests-ack",
|
|
1380
|
-
interestRevision: 2,
|
|
1381
|
-
sequence: 0,
|
|
1382
|
-
unavailableInterests: [],
|
|
1383
|
-
});
|
|
1384
|
-
await settle();
|
|
1385
|
-
expect(client.getSnapshot().phase).toBe("live");
|
|
1386
|
-
|
|
1387
|
-
const renamedShell = ApplicationShellProjectionInputV3SchemaZ.parse({
|
|
1388
|
-
...currentShell,
|
|
1389
|
-
terminalInventory: {
|
|
1390
|
-
...currentShell.terminalInventory,
|
|
1391
|
-
resources: currentShell.terminalInventory.resources.map((resource) => ({
|
|
1392
|
-
...resource,
|
|
1393
|
-
title: "Lifecycle Renamed",
|
|
1394
|
-
})),
|
|
1395
|
-
},
|
|
1396
|
-
});
|
|
1397
|
-
currentShell = renamedShell;
|
|
1398
|
-
const operationId = "19000000-0000-4000-8000-000000000019";
|
|
1399
|
-
socket.frame({
|
|
1400
|
-
type: "resource.changed",
|
|
1401
|
-
sequence: 1,
|
|
1402
|
-
workspaceName: "alpha",
|
|
1403
|
-
resource: "application-shell",
|
|
1404
|
-
revision: 8,
|
|
1405
|
-
causeOperationId: operationId,
|
|
1406
|
-
});
|
|
1407
|
-
expect(client.getSnapshot().operations.lastResourceChangeAcknowledgement).toEqual({
|
|
1408
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
1409
|
-
operationId,
|
|
1410
|
-
sequence: 1,
|
|
1411
|
-
revision: 8,
|
|
1412
|
-
});
|
|
1413
|
-
expect(client.getSnapshot().semantic?.terminalInventory?.resources[0]?.title).toBe(
|
|
1414
|
-
"Terminal 1",
|
|
1415
|
-
);
|
|
1416
|
-
await settle();
|
|
1417
|
-
expect(client.getSnapshot().semantic?.terminalInventory?.resources[0]?.title).toBe(
|
|
1418
|
-
"Lifecycle Renamed",
|
|
1419
|
-
);
|
|
1420
|
-
expect(client.getSnapshot().operations.lastReceipt).toBeNull();
|
|
1421
|
-
await client.dispose();
|
|
1422
|
-
transport.disposeEventSupervisor();
|
|
1423
|
-
});
|
|
1424
|
-
|
|
1425
|
-
it("suspends only the exact active runtime while its supervisor reconnects", async () => {
|
|
1426
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
1427
|
-
const first = new ClosableFakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1428
|
-
const replacement = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1429
|
-
const suspended: FakeRuntime[] = [];
|
|
1430
|
-
let connects = 0;
|
|
1431
|
-
const client = createWorkspaceClient({
|
|
1432
|
-
target: target("alpha"),
|
|
1433
|
-
ports: {
|
|
1434
|
-
shell: shell.transport,
|
|
1435
|
-
connectRuntime: async () => (++connects === 1 ? first : replacement),
|
|
1436
|
-
didSuspendRuntime: (runtime) => suspended.push(runtime as FakeRuntime),
|
|
1437
|
-
actions,
|
|
1438
|
-
},
|
|
1439
|
-
});
|
|
1440
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1441
|
-
await settle();
|
|
1442
|
-
expect(client.ownsRuntimeAuthority("input")).toBe(false);
|
|
1443
|
-
|
|
1444
|
-
first.fail();
|
|
1445
|
-
await settle();
|
|
1446
|
-
expect(suspended).toEqual([first]);
|
|
1447
|
-
expect(connects).toBe(1);
|
|
1448
|
-
expect(await client.requestAuthority("input")).toBeNull();
|
|
1449
|
-
expect(replacement.closeCount).toBe(0);
|
|
1450
|
-
|
|
1451
|
-
await client.dispose();
|
|
1452
|
-
});
|
|
1453
|
-
|
|
1454
|
-
it("isolates overlapping candidate preparation and publishes only the winning runtime", async () => {
|
|
1455
|
-
const shell = shellBroker({ alpha: shellResource("alpha", ["pane.alpha"]) });
|
|
1456
|
-
const active = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1457
|
-
const candidateB = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1458
|
-
const candidateC = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1459
|
-
const coherentB = deferred<void>();
|
|
1460
|
-
const coherentC = deferred<void>();
|
|
1461
|
-
const preparedB = deferred<void>();
|
|
1462
|
-
const preparedC = deferred<void>();
|
|
1463
|
-
const publications: string[] = [];
|
|
1464
|
-
const activated: FakeRuntime[] = [];
|
|
1465
|
-
const client = createWorkspaceClient<string, string>({
|
|
1466
|
-
target: target("alpha"),
|
|
1467
|
-
ports: {
|
|
1468
|
-
shell: shell.transport,
|
|
1469
|
-
connectRuntime: async (_current, inventory, _signal, prepare) => {
|
|
1470
|
-
if (inventory.semanticPaneIds.includes("pane.b")) {
|
|
1471
|
-
await prepare(candidateB);
|
|
1472
|
-
preparedB.resolve();
|
|
1473
|
-
await coherentB.promise;
|
|
1474
|
-
return candidateB;
|
|
1475
|
-
}
|
|
1476
|
-
if (inventory.semanticPaneIds.includes("pane.c")) {
|
|
1477
|
-
await prepare(candidateC);
|
|
1478
|
-
preparedC.resolve();
|
|
1479
|
-
await coherentC.promise;
|
|
1480
|
-
return candidateC;
|
|
1481
|
-
}
|
|
1482
|
-
return active;
|
|
1483
|
-
},
|
|
1484
|
-
didActivateRuntime: (runtime) => activated.push(runtime as FakeRuntime),
|
|
1485
|
-
actions,
|
|
1486
|
-
},
|
|
1487
|
-
});
|
|
1488
|
-
client.subscribeTerminal({ workspaceName: "alpha", semanticPaneId: "pane.alpha" }, (update) =>
|
|
1489
|
-
publications.push(update.snapshot ?? update.patch ?? "tombstone"),
|
|
1490
|
-
);
|
|
1491
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1492
|
-
await settle();
|
|
1493
|
-
expect(activated).toEqual([active]);
|
|
1494
|
-
const activeSubscription = active.subscriptions.get("pane.alpha")!;
|
|
1495
|
-
const activeClose = deferred<void>();
|
|
1496
|
-
activeSubscription.closeGate = activeClose.promise;
|
|
1497
|
-
|
|
1498
|
-
shell.byWorkspace.set("alpha", shellResource("alpha", ["pane.alpha", "pane.b"]));
|
|
1499
|
-
shell.connections[0]!.handlers.onInvalidate();
|
|
1500
|
-
await preparedB.promise;
|
|
1501
|
-
candidateB.subscriptions
|
|
1502
|
-
.get("pane.alpha")!
|
|
1503
|
-
.emit({ ...terminalSeed(ALPHA_DAEMON.instanceId), snapshot: "candidate-b" });
|
|
1504
|
-
expect(publications).toEqual([]);
|
|
1505
|
-
|
|
1506
|
-
shell.byWorkspace.set("alpha", shellResource("alpha", ["pane.alpha", "pane.c"]));
|
|
1507
|
-
shell.connections[0]!.handlers.onInvalidate();
|
|
1508
|
-
await preparedC.promise;
|
|
1509
|
-
candidateC.subscriptions
|
|
1510
|
-
.get("pane.alpha")!
|
|
1511
|
-
.emit({ ...terminalSeed(ALPHA_DAEMON.instanceId), snapshot: "candidate-c" });
|
|
1512
|
-
expect(publications).toEqual([]);
|
|
1513
|
-
|
|
1514
|
-
coherentC.resolve();
|
|
1515
|
-
await settle();
|
|
1516
|
-
expect(activated).toEqual([active, candidateC]);
|
|
1517
|
-
expect(publications).toEqual(["candidate-c"]);
|
|
1518
|
-
expect(candidateB.subscriptions.get("pane.alpha")!.closeCount).toBe(1);
|
|
1519
|
-
expect(activeSubscription.closeCount).toBe(1);
|
|
1520
|
-
expect(activeSubscription.listeners.size).toBe(0);
|
|
1521
|
-
activeSubscription.emit({
|
|
1522
|
-
...terminalSeed(ALPHA_DAEMON.instanceId),
|
|
1523
|
-
snapshot: "retired-active",
|
|
1524
|
-
});
|
|
1525
|
-
expect(publications).toEqual(["candidate-c"]);
|
|
1526
|
-
candidateC.subscriptions.get("pane.alpha")!.emit({
|
|
1527
|
-
...terminalSeed(ALPHA_DAEMON.instanceId),
|
|
1528
|
-
snapshot: "candidate-c-live",
|
|
1529
|
-
});
|
|
1530
|
-
expect(publications).toEqual(["candidate-c", "candidate-c-live"]);
|
|
1531
|
-
activeClose.resolve();
|
|
1532
|
-
|
|
1533
|
-
coherentB.resolve();
|
|
1534
|
-
await settle();
|
|
1535
|
-
expect(activated).toEqual([active, candidateC]);
|
|
1536
|
-
expect(publications).toEqual(["candidate-c", "candidate-c-live"]);
|
|
1537
|
-
expect(candidateB.closeCount).toBe(1);
|
|
1538
|
-
await client.dispose();
|
|
1539
|
-
});
|
|
1540
|
-
|
|
1541
|
-
it("stages the production fast-lane inventory before candidate coherence without trimming the incumbent", async () => {
|
|
1542
|
-
const shell = shellBroker({ alpha: shellResource("alpha", ["pane.alpha"]) });
|
|
1543
|
-
const active = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1544
|
-
const candidate = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1545
|
-
const candidateCoherent = deferred<void>();
|
|
1546
|
-
const candidatePrepared = deferred<void>();
|
|
1547
|
-
const staged = new WeakMap<FakeRuntime, () => void>();
|
|
1548
|
-
let lane!: TerminalFastLane;
|
|
1549
|
-
let client!: ReturnType<typeof createWorkspaceClient<string, string>>;
|
|
1550
|
-
client = createWorkspaceClient<string, string>({
|
|
1551
|
-
target: target("alpha"),
|
|
1552
|
-
ports: {
|
|
1553
|
-
shell: shell.transport,
|
|
1554
|
-
connectRuntime: async (_current, inventory, _signal, prepare) => {
|
|
1555
|
-
const runtime = inventory.semanticPaneIds.includes("pane.beta") ? candidate : active;
|
|
1556
|
-
const release = lane.stagePanes(inventory.semanticPaneIds);
|
|
1557
|
-
await prepare(runtime);
|
|
1558
|
-
staged.set(runtime, release);
|
|
1559
|
-
if (runtime === candidate) {
|
|
1560
|
-
candidatePrepared.resolve();
|
|
1561
|
-
await candidateCoherent.promise;
|
|
1562
|
-
}
|
|
1563
|
-
return runtime;
|
|
1564
|
-
},
|
|
1565
|
-
didActivateRuntime: (runtime, inventory) => {
|
|
1566
|
-
lane.retainPanes(inventory.semanticPaneIds);
|
|
1567
|
-
const release = staged.get(runtime as FakeRuntime);
|
|
1568
|
-
staged.delete(runtime as FakeRuntime);
|
|
1569
|
-
release?.();
|
|
1570
|
-
},
|
|
1571
|
-
actions,
|
|
1572
|
-
},
|
|
1573
|
-
});
|
|
1574
|
-
lane = createTerminalFastLane({
|
|
1575
|
-
address: { workspaceName: "alpha", generation: ALPHA_DAEMON.instanceId },
|
|
1576
|
-
source: createWorkspaceClientTerminalSource(client as never),
|
|
1577
|
-
repair: { request: () => undefined },
|
|
1578
|
-
control: {
|
|
1579
|
-
owns: () => true,
|
|
1580
|
-
request: async () => true,
|
|
1581
|
-
write: async () => "ok",
|
|
1582
|
-
resize: async () => "ok",
|
|
1583
|
-
},
|
|
1584
|
-
});
|
|
1585
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1586
|
-
await settle();
|
|
1587
|
-
expect(active.subscriptions.has("pane.alpha")).toBe(true);
|
|
1588
|
-
|
|
1589
|
-
shell.byWorkspace.set("alpha", shellResource("alpha", ["pane.alpha", "pane.beta"]));
|
|
1590
|
-
shell.connections[0]!.handlers.onInvalidate();
|
|
1591
|
-
await candidatePrepared.promise;
|
|
1592
|
-
// This is the production-order proof: prepare() saw the new fast-lane
|
|
1593
|
-
// interest, while the incumbent remained subscribed and paint-capable.
|
|
1594
|
-
expect(candidate.subscriptions.has("pane.alpha")).toBe(true);
|
|
1595
|
-
expect(candidate.subscriptions.has("pane.beta")).toBe(true);
|
|
1596
|
-
expect(active.subscriptions.get("pane.alpha")?.listeners.size).toBe(1);
|
|
1597
|
-
|
|
1598
|
-
candidateCoherent.resolve();
|
|
1599
|
-
await settle();
|
|
1600
|
-
expect(active.subscriptions.get("pane.alpha")?.listeners.size).toBe(0);
|
|
1601
|
-
expect(candidate.subscriptions.get("pane.alpha")?.listeners.size).toBe(1);
|
|
1602
|
-
expect(candidate.subscriptions.get("pane.beta")?.listeners.size).toBe(1);
|
|
1603
|
-
lane.dispose();
|
|
1604
|
-
await client.dispose();
|
|
1605
|
-
});
|
|
1606
|
-
|
|
1607
|
-
it("retires a flooded candidate without dropping, reordering, or publishing partial state", async () => {
|
|
1608
|
-
const shell = shellBroker({ alpha: shellResource("alpha", ["pane.alpha"]) });
|
|
1609
|
-
const active = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1610
|
-
const flooded = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1611
|
-
const prepared = deferred<void>();
|
|
1612
|
-
const coherent = deferred<void>();
|
|
1613
|
-
const publications: string[] = [];
|
|
1614
|
-
const activated: FakeRuntime[] = [];
|
|
1615
|
-
const client = createWorkspaceClient<string, string>({
|
|
1616
|
-
target: target("alpha"),
|
|
1617
|
-
ports: {
|
|
1618
|
-
shell: shell.transport,
|
|
1619
|
-
connectRuntime: async (_current, inventory, _signal, prepare) => {
|
|
1620
|
-
if (!inventory.semanticPaneIds.includes("pane.flood")) return active;
|
|
1621
|
-
await prepare(flooded);
|
|
1622
|
-
prepared.resolve();
|
|
1623
|
-
await coherent.promise;
|
|
1624
|
-
return flooded;
|
|
1625
|
-
},
|
|
1626
|
-
didActivateRuntime: (runtime) => activated.push(runtime as FakeRuntime),
|
|
1627
|
-
actions,
|
|
1628
|
-
},
|
|
1629
|
-
});
|
|
1630
|
-
client.subscribeTerminal({ workspaceName: "alpha", semanticPaneId: "pane.alpha" }, (update) =>
|
|
1631
|
-
publications.push(update.snapshot ?? update.patch ?? "tombstone"),
|
|
1632
|
-
);
|
|
1633
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1634
|
-
await settle();
|
|
1635
|
-
expect(activated).toEqual([active]);
|
|
1636
|
-
|
|
1637
|
-
shell.byWorkspace.set("alpha", shellResource("alpha", ["pane.alpha", "pane.flood"]));
|
|
1638
|
-
shell.connections[0]!.handlers.onInvalidate();
|
|
1639
|
-
await prepared.promise;
|
|
1640
|
-
const subscription = flooded.subscriptions.get("pane.alpha")!;
|
|
1641
|
-
for (let index = 0; index <= 256; index += 1) {
|
|
1642
|
-
subscription.emit({
|
|
1643
|
-
...terminalSeed(ALPHA_DAEMON.instanceId),
|
|
1644
|
-
snapshot: `flood-${index}`,
|
|
1645
|
-
});
|
|
1646
|
-
}
|
|
1647
|
-
await settle();
|
|
1648
|
-
expect(publications).toEqual([]);
|
|
1649
|
-
expect(activated).toEqual([active]);
|
|
1650
|
-
expect(subscription.closeCount).toBe(1);
|
|
1651
|
-
expect(subscription.listeners.size).toBe(0);
|
|
1652
|
-
|
|
1653
|
-
coherent.resolve();
|
|
1654
|
-
await settle();
|
|
1655
|
-
expect(flooded.closeCount).toBe(1);
|
|
1656
|
-
expect(activated).toEqual([active]);
|
|
1657
|
-
expect(
|
|
1658
|
-
await client.sendTerminalInput(
|
|
1659
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.alpha" },
|
|
1660
|
-
{ kind: "text", data: "incumbent-still-live" },
|
|
1661
|
-
),
|
|
1662
|
-
).toBe("ok");
|
|
1663
|
-
expect(active.inputs).toHaveLength(1);
|
|
1664
|
-
await client.dispose();
|
|
1665
|
-
});
|
|
1666
|
-
|
|
1667
|
-
it("retires the runtime cleanly when authority publishes an empty inventory", async () => {
|
|
1668
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
1669
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1670
|
-
let connects = 0;
|
|
1671
|
-
let retirements = 0;
|
|
1672
|
-
const client = createWorkspaceClient({
|
|
1673
|
-
target: target("alpha"),
|
|
1674
|
-
ports: {
|
|
1675
|
-
shell: shell.transport,
|
|
1676
|
-
connectRuntime: async () => {
|
|
1677
|
-
connects += 1;
|
|
1678
|
-
return runtime;
|
|
1679
|
-
},
|
|
1680
|
-
didRetireRuntime: () => {
|
|
1681
|
-
retirements += 1;
|
|
1682
|
-
},
|
|
1683
|
-
actions,
|
|
1684
|
-
},
|
|
1685
|
-
});
|
|
1686
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1687
|
-
await settle();
|
|
1688
|
-
shell.byWorkspace.set("alpha", shellResource("alpha", []));
|
|
1689
|
-
shell.connections[0]!.handlers.onInvalidate();
|
|
1690
|
-
await settle();
|
|
1691
|
-
|
|
1692
|
-
expect(connects).toBe(1);
|
|
1693
|
-
expect(runtime.closeCount).toBe(1);
|
|
1694
|
-
expect(retirements).toBe(1);
|
|
1695
|
-
expect(
|
|
1696
|
-
await client.sendTerminalInput(
|
|
1697
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.alpha" },
|
|
1698
|
-
{ kind: "key", data: "Enter" },
|
|
1699
|
-
),
|
|
1700
|
-
).toBe("authority-lost");
|
|
1701
|
-
client.dispose();
|
|
1702
|
-
await settle();
|
|
1703
|
-
});
|
|
1704
|
-
|
|
1705
|
-
it("fences a late runtime candidate after target replacement", async () => {
|
|
1706
|
-
const shell = shellBroker({
|
|
1707
|
-
alpha: shellResource("alpha"),
|
|
1708
|
-
beta: shellResource("beta"),
|
|
1709
|
-
});
|
|
1710
|
-
const alpha = deferred<FakeRuntime>();
|
|
1711
|
-
const staleAlpha = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1712
|
-
const beta = new FakeRuntime(BETA_DAEMON.instanceId);
|
|
1713
|
-
const calls: string[] = [];
|
|
1714
|
-
const client = createWorkspaceClient({
|
|
1715
|
-
target: target("alpha"),
|
|
1716
|
-
ports: {
|
|
1717
|
-
shell: shell.transport,
|
|
1718
|
-
connectRuntime: async (current) => {
|
|
1719
|
-
calls.push(current.workspaceName);
|
|
1720
|
-
return current.workspaceName === "alpha" ? alpha.promise : beta;
|
|
1721
|
-
},
|
|
1722
|
-
actions,
|
|
1723
|
-
},
|
|
1724
|
-
});
|
|
1725
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1726
|
-
await settle();
|
|
1727
|
-
expect(calls).toEqual(["alpha"]);
|
|
1728
|
-
|
|
1729
|
-
client.setTarget(target("beta", BETA_DAEMON));
|
|
1730
|
-
alpha.resolve(staleAlpha);
|
|
1731
|
-
shell.connections[1]!.handlers.onVerifiedOpen();
|
|
1732
|
-
await settle();
|
|
1733
|
-
|
|
1734
|
-
expect(calls).toEqual(["alpha", "beta"]);
|
|
1735
|
-
expect(staleAlpha.closeCount).toBe(1);
|
|
1736
|
-
expect(
|
|
1737
|
-
await client.sendTerminalInput(
|
|
1738
|
-
{ workspaceName: "beta", semanticPaneId: "pane.beta" },
|
|
1739
|
-
{ kind: "key", data: "Enter" },
|
|
1740
|
-
),
|
|
1741
|
-
).toBe("ok");
|
|
1742
|
-
expect(beta.inputs).toHaveLength(1);
|
|
1743
|
-
client.dispose();
|
|
1744
|
-
await settle();
|
|
1745
|
-
});
|
|
1746
|
-
|
|
1747
|
-
it("ignores a delayed catalog read from a retired target generation", async () => {
|
|
1748
|
-
const alphaCatalog = deferred<ReturnType<typeof workspaceCatalog>>();
|
|
1749
|
-
const betaCatalog = deferred<ReturnType<typeof workspaceCatalog>>();
|
|
1750
|
-
const shell = shellBroker({ alpha: shellResource("alpha"), beta: shellResource("beta") });
|
|
1751
|
-
const client = createWorkspaceClient({
|
|
1752
|
-
target: target("alpha"),
|
|
1753
|
-
ports: {
|
|
1754
|
-
shell: shell.transport,
|
|
1755
|
-
catalog: {
|
|
1756
|
-
read: (current) =>
|
|
1757
|
-
current.workspaceName === "alpha" ? alphaCatalog.promise : betaCatalog.promise,
|
|
1758
|
-
subscribe: () => ({ close: () => undefined }),
|
|
1759
|
-
},
|
|
1760
|
-
connectRuntime: async (current) => new FakeRuntime(current.daemon.instanceId),
|
|
1761
|
-
actions,
|
|
1762
|
-
},
|
|
1763
|
-
});
|
|
1764
|
-
await settle();
|
|
1765
|
-
const replacing = client.setTarget(target("beta", BETA_DAEMON));
|
|
1766
|
-
await settle();
|
|
1767
|
-
alphaCatalog.resolve(workspaceCatalog("alpha"));
|
|
1768
|
-
betaCatalog.resolve(workspaceCatalog("beta", BETA_DAEMON));
|
|
1769
|
-
await replacing;
|
|
1770
|
-
await settle();
|
|
1771
|
-
expect(client.getSnapshot().catalog).toMatchObject({
|
|
1772
|
-
daemonInstanceId: BETA_DAEMON.instanceId,
|
|
1773
|
-
intents: [{ workspaceName: "beta" }],
|
|
1774
|
-
liveSessions: [{ sessionName: "beta" }],
|
|
1775
|
-
});
|
|
1776
|
-
await client.dispose();
|
|
1777
|
-
});
|
|
1778
|
-
|
|
1779
|
-
it("settles target replacement even when the retired connect adapter never resolves", async () => {
|
|
1780
|
-
const shell = shellBroker({
|
|
1781
|
-
alpha: shellResource("alpha"),
|
|
1782
|
-
beta: shellResource("beta"),
|
|
1783
|
-
});
|
|
1784
|
-
const never = new Promise<FakeRuntime>(() => undefined);
|
|
1785
|
-
const beta = new FakeRuntime(BETA_DAEMON.instanceId);
|
|
1786
|
-
const client = createWorkspaceClient({
|
|
1787
|
-
target: target("alpha"),
|
|
1788
|
-
ports: {
|
|
1789
|
-
shell: shell.transport,
|
|
1790
|
-
connectRuntime: (current) =>
|
|
1791
|
-
current.workspaceName === "alpha" ? never : Promise.resolve(beta),
|
|
1792
|
-
actions,
|
|
1793
|
-
},
|
|
1794
|
-
});
|
|
1795
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1796
|
-
await settle();
|
|
1797
|
-
|
|
1798
|
-
await client.setTarget(target("beta", BETA_DAEMON));
|
|
1799
|
-
shell.connections[1]!.handlers.onVerifiedOpen();
|
|
1800
|
-
await settle();
|
|
1801
|
-
expect(client.getSnapshot().target?.workspaceName).toBe("beta");
|
|
1802
|
-
await client.dispose();
|
|
1803
|
-
});
|
|
1804
|
-
|
|
1805
|
-
it("fences terminal repair to the active daemon generation", async () => {
|
|
1806
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
1807
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1808
|
-
const client = createWorkspaceClient({
|
|
1809
|
-
target: target("alpha"),
|
|
1810
|
-
ports: { shell: shell.transport, connectRuntime: async () => runtime, actions },
|
|
1811
|
-
});
|
|
1812
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1813
|
-
await settle();
|
|
1814
|
-
const pane = { workspaceName: "alpha", semanticPaneId: "pane.alpha" };
|
|
1815
|
-
|
|
1816
|
-
client.requestTerminalRepair(pane, BETA_DAEMON.instanceId, "conflict");
|
|
1817
|
-
client.requestTerminalRepair(pane, ALPHA_DAEMON.instanceId, "conflict");
|
|
1818
|
-
expect(runtime.repairRequests).toEqual([{ target: pane, reason: "conflict" }]);
|
|
1819
|
-
await client.dispose();
|
|
1820
|
-
});
|
|
1821
|
-
|
|
1822
|
-
it("produces byte-identical semantic traces for core, OpenTUI, Web, and SDK adapters", async () => {
|
|
1823
|
-
const labels = ["core", "opentui", "web", "sdk"] as const;
|
|
1824
|
-
const clients = labels.map((surface) => {
|
|
1825
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
1826
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1827
|
-
const clock = new FakeClock();
|
|
1828
|
-
const client = createWorkspaceClient({
|
|
1829
|
-
target: target("alpha"),
|
|
1830
|
-
ports: {
|
|
1831
|
-
shell: shell.transport,
|
|
1832
|
-
connectRuntime: async () => runtime,
|
|
1833
|
-
actions,
|
|
1834
|
-
},
|
|
1835
|
-
clock,
|
|
1836
|
-
operationId: () => "10000000-0000-4000-8000-000000000001",
|
|
1837
|
-
});
|
|
1838
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1839
|
-
return {
|
|
1840
|
-
client,
|
|
1841
|
-
shell,
|
|
1842
|
-
runtime,
|
|
1843
|
-
adapter: createWorkspaceClientConformanceAdapter(surface, client),
|
|
1844
|
-
};
|
|
1845
|
-
});
|
|
1846
|
-
await settle();
|
|
1847
|
-
const liveTraceHashes = clients.map(({ adapter }) => adapter.traceHash());
|
|
1848
|
-
expect(new Set(liveTraceHashes).size).toBe(1);
|
|
1849
|
-
|
|
1850
|
-
const invocation = applicationShellCommandInvocation(
|
|
1851
|
-
APPLICATION_SHELL_COMMAND_IDS.activateMode,
|
|
1852
|
-
{ mode: "home" },
|
|
1853
|
-
{ kind: "program", surface: "workspace-client-conformance" },
|
|
1854
|
-
);
|
|
1855
|
-
for (const { client } of clients) {
|
|
1856
|
-
await client.dispatch({ kind: "application-shell", invocation });
|
|
1857
|
-
}
|
|
1858
|
-
const localProjectionTraceHashes = clients.map(({ adapter }) => adapter.traceHash());
|
|
1859
|
-
expect(new Set(localProjectionTraceHashes).size).toBe(1);
|
|
1860
|
-
|
|
1861
|
-
const operationId = "10000000-0000-4000-8000-000000000001";
|
|
1862
|
-
for (const { client } of clients) {
|
|
1863
|
-
await client.dispatch({
|
|
1864
|
-
kind: "semantic-intent",
|
|
1865
|
-
operationId,
|
|
1866
|
-
intent: {
|
|
1867
|
-
verb: "workspace.pane.read",
|
|
1868
|
-
workspaceName: "alpha",
|
|
1869
|
-
semanticPaneId: "pane.alpha",
|
|
1870
|
-
origin: "sdk",
|
|
1871
|
-
},
|
|
1872
|
-
});
|
|
1873
|
-
}
|
|
1874
|
-
for (const { runtime } of clients) {
|
|
1875
|
-
runtime.emitReceipt(receipt(operationId, "accepted"));
|
|
1876
|
-
runtime.emitReceipt(receipt(operationId, "observed"));
|
|
1877
|
-
}
|
|
1878
|
-
const receiptTraceHashes = clients.map(({ adapter }) => adapter.traceHash());
|
|
1879
|
-
expect(new Set(receiptTraceHashes).size).toBe(1);
|
|
1880
|
-
|
|
1881
|
-
const beforeTerminal = clients.map(({ adapter }) => adapter.traceHash());
|
|
1882
|
-
for (const { client, runtime } of clients) {
|
|
1883
|
-
client.subscribeTerminal(
|
|
1884
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.alpha" },
|
|
1885
|
-
() => undefined,
|
|
1886
|
-
);
|
|
1887
|
-
await settle();
|
|
1888
|
-
runtime.subscriptions.get("pane.alpha")!.emit(terminalSeed(ALPHA_DAEMON.instanceId));
|
|
1889
|
-
}
|
|
1890
|
-
expect(clients.map(({ adapter }) => adapter.traceHash())).toEqual(beforeTerminal);
|
|
1891
|
-
|
|
1892
|
-
const bytes = clients.map(({ adapter }) => adapter.semanticBytes());
|
|
1893
|
-
const hashes = clients.map(({ adapter }) => adapter.semanticHash());
|
|
1894
|
-
expect(new Set(bytes).size).toBe(1);
|
|
1895
|
-
expect(new Set(hashes).size).toBe(1);
|
|
1896
|
-
expect(clients[0]!.client.getSnapshot().authorityShell?.appWindows.revision).toBe(7);
|
|
1897
|
-
for (const { adapter, client } of clients) {
|
|
1898
|
-
adapter.dispose();
|
|
1899
|
-
client.dispose();
|
|
1900
|
-
}
|
|
1901
|
-
});
|
|
1902
|
-
|
|
1903
|
-
it("fences stale target work, terminal frames, receipts, and timers", async () => {
|
|
1904
|
-
const clock = new FakeClock();
|
|
1905
|
-
const shell = shellBroker({ alpha: shellResource("alpha"), beta: shellResource("beta") });
|
|
1906
|
-
let resolveAlpha!: (runtime: FakeRuntime) => void;
|
|
1907
|
-
const alphaRuntimePromise = new Promise<FakeRuntime>((resolve) => (resolveAlpha = resolve));
|
|
1908
|
-
const betaRuntime = new FakeRuntime(BETA_DAEMON.instanceId);
|
|
1909
|
-
const client = createWorkspaceClient({
|
|
1910
|
-
target: target("alpha"),
|
|
1911
|
-
ports: {
|
|
1912
|
-
shell: shell.transport,
|
|
1913
|
-
connectRuntime: (current) =>
|
|
1914
|
-
current.workspaceName === "alpha" ? alphaRuntimePromise : Promise.resolve(betaRuntime),
|
|
1915
|
-
actions,
|
|
1916
|
-
},
|
|
1917
|
-
clock,
|
|
1918
|
-
operationTimeoutMs: 50,
|
|
1919
|
-
});
|
|
1920
|
-
const generations: number[] = [];
|
|
1921
|
-
client.subscribe("lifecycle", ({ generation }) => generations.push(generation));
|
|
1922
|
-
const staleAlphaRuntime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1923
|
-
client.setTarget(target("beta", BETA_DAEMON));
|
|
1924
|
-
client.setTarget(target("beta", BETA_DAEMON));
|
|
1925
|
-
resolveAlpha(staleAlphaRuntime);
|
|
1926
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1927
|
-
shell.connections[0]!.handlers.onInvalidate();
|
|
1928
|
-
await settle();
|
|
1929
|
-
|
|
1930
|
-
expect(client.getSnapshot()).toMatchObject({
|
|
1931
|
-
generation: 2,
|
|
1932
|
-
target: { workspaceName: "beta" },
|
|
1933
|
-
});
|
|
1934
|
-
expect(Math.max(...generations)).toBe(2);
|
|
1935
|
-
// Inventory authority never arrived for alpha, so no speculative runtime was opened.
|
|
1936
|
-
expect(staleAlphaRuntime.closeCount).toBe(0);
|
|
1937
|
-
expect(client.getSnapshot().authorityShell?.project.name).toBe("beta");
|
|
1938
|
-
clock.advance(10_000);
|
|
1939
|
-
expect(client.getSnapshot().generation).toBe(2);
|
|
1940
|
-
client.dispose();
|
|
1941
|
-
});
|
|
1942
|
-
|
|
1943
|
-
it("routes canonical terminal input through one runtime and fences late generation acks", async () => {
|
|
1944
|
-
const shell = shellBroker({ alpha: shellResource("alpha"), beta: shellResource("beta") });
|
|
1945
|
-
const alphaRuntime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1946
|
-
const betaRuntime = new FakeRuntime(BETA_DAEMON.instanceId);
|
|
1947
|
-
const client = createWorkspaceClient({
|
|
1948
|
-
target: target("alpha"),
|
|
1949
|
-
ports: {
|
|
1950
|
-
shell: shell.transport,
|
|
1951
|
-
connectRuntime: async (current) =>
|
|
1952
|
-
current.workspaceName === "alpha" ? alphaRuntime : betaRuntime,
|
|
1953
|
-
actions,
|
|
1954
|
-
},
|
|
1955
|
-
});
|
|
1956
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
1957
|
-
await settle();
|
|
1958
|
-
|
|
1959
|
-
expect(
|
|
1960
|
-
await Promise.all([
|
|
1961
|
-
client.sendTerminalInput(
|
|
1962
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.alpha" },
|
|
1963
|
-
{ kind: "text", data: "hello" },
|
|
1964
|
-
),
|
|
1965
|
-
client.sendTerminalInput(
|
|
1966
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.alpha" },
|
|
1967
|
-
{ kind: "key", data: "Enter" },
|
|
1968
|
-
),
|
|
1969
|
-
]),
|
|
1970
|
-
).toEqual(["ok", "ok"]);
|
|
1971
|
-
expect(alphaRuntime.inputs.map(({ input }) => input)).toEqual([
|
|
1972
|
-
{ kind: "text", data: "hello" },
|
|
1973
|
-
{ kind: "key", data: "Enter" },
|
|
1974
|
-
]);
|
|
1975
|
-
|
|
1976
|
-
const lateAck = deferred<void>();
|
|
1977
|
-
alphaRuntime.inputGate = lateAck.promise;
|
|
1978
|
-
const stale = client.sendTerminalInput(
|
|
1979
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.alpha" },
|
|
1980
|
-
{ kind: "key", data: "C-c" },
|
|
1981
|
-
);
|
|
1982
|
-
client.setTarget(target("beta", BETA_DAEMON));
|
|
1983
|
-
lateAck.resolve();
|
|
1984
|
-
expect(await stale).toBe("authority-lost");
|
|
1985
|
-
expect(
|
|
1986
|
-
await client.sendTerminalInput(
|
|
1987
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.alpha" },
|
|
1988
|
-
{ kind: "key", data: "Up" },
|
|
1989
|
-
),
|
|
1990
|
-
).toBe("authority-lost");
|
|
1991
|
-
client.dispose();
|
|
1992
|
-
});
|
|
1993
|
-
|
|
1994
|
-
it("fits the shared runtime viewport and fences a late retired generation", async () => {
|
|
1995
|
-
const shell = shellBroker({ alpha: shellResource("alpha"), beta: shellResource("beta") });
|
|
1996
|
-
const alphaRuntime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
1997
|
-
const betaRuntime = new FakeRuntime(BETA_DAEMON.instanceId);
|
|
1998
|
-
const lateFit = deferred<void>();
|
|
1999
|
-
alphaRuntime.viewportGate = lateFit.promise;
|
|
2000
|
-
const client = createWorkspaceClient({
|
|
2001
|
-
target: target("alpha"),
|
|
2002
|
-
ports: {
|
|
2003
|
-
shell: shell.transport,
|
|
2004
|
-
connectRuntime: async (current) =>
|
|
2005
|
-
current.workspaceName === "alpha" ? alphaRuntime : betaRuntime,
|
|
2006
|
-
actions,
|
|
2007
|
-
},
|
|
2008
|
-
});
|
|
2009
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2010
|
-
await settle();
|
|
2011
|
-
|
|
2012
|
-
const retiredFit = client.fitViewport(132, 44);
|
|
2013
|
-
client.setTarget(target("beta", BETA_DAEMON));
|
|
2014
|
-
lateFit.resolve();
|
|
2015
|
-
shell.connections[1]!.handlers.onVerifiedOpen();
|
|
2016
|
-
await settle();
|
|
2017
|
-
expect(await retiredFit).toBe("authority-lost");
|
|
2018
|
-
expect(alphaRuntime.viewportFits).toEqual([{ cols: 132, rows: 44 }]);
|
|
2019
|
-
expect(await client.fitViewport(100, 30)).toBe("ok");
|
|
2020
|
-
expect(betaRuntime.viewportFits).toEqual([{ cols: 100, rows: 30 }]);
|
|
2021
|
-
client.dispose();
|
|
2022
|
-
await settle();
|
|
2023
|
-
});
|
|
2024
|
-
|
|
2025
|
-
it("preserves a scoped window target and fences its retired generation", async () => {
|
|
2026
|
-
const shell = shellBroker({ alpha: shellResource("alpha"), beta: shellResource("beta") });
|
|
2027
|
-
const alphaRuntime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2028
|
-
const betaRuntime = new FakeRuntime(BETA_DAEMON.instanceId);
|
|
2029
|
-
const lateFit = deferred<void>();
|
|
2030
|
-
alphaRuntime.viewportGate = lateFit.promise;
|
|
2031
|
-
const client = createWorkspaceClient({
|
|
2032
|
-
target: target("alpha"),
|
|
2033
|
-
ports: {
|
|
2034
|
-
shell: shell.transport,
|
|
2035
|
-
connectRuntime: async (current) =>
|
|
2036
|
-
current.workspaceName === "alpha" ? alphaRuntime : betaRuntime,
|
|
2037
|
-
actions,
|
|
2038
|
-
},
|
|
2039
|
-
});
|
|
2040
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2041
|
-
await settle();
|
|
2042
|
-
|
|
2043
|
-
const retiredFit = client.fitViewport(132, 44, "window.alpha");
|
|
2044
|
-
client.setTarget(target("beta", BETA_DAEMON));
|
|
2045
|
-
lateFit.resolve();
|
|
2046
|
-
shell.connections[1]!.handlers.onVerifiedOpen();
|
|
2047
|
-
await settle();
|
|
2048
|
-
expect(await retiredFit).toBe("authority-lost");
|
|
2049
|
-
expect(alphaRuntime.viewportFits).toEqual([
|
|
2050
|
-
{ cols: 132, rows: 44, semanticWindowId: "window.alpha" },
|
|
2051
|
-
]);
|
|
2052
|
-
expect(await client.fitViewport(100, 30)).toBe("ok");
|
|
2053
|
-
expect(betaRuntime.viewportFits).toEqual([{ cols: 100, rows: 30 }]);
|
|
2054
|
-
client.dispose();
|
|
2055
|
-
await settle();
|
|
2056
|
-
});
|
|
2057
|
-
|
|
2058
|
-
it("preserves an exact geometry-authority conflict from the current runtime", async () => {
|
|
2059
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
2060
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2061
|
-
runtime.viewportResult = "geometry-authority-conflict";
|
|
2062
|
-
const client = createWorkspaceClient({
|
|
2063
|
-
target: target("alpha"),
|
|
2064
|
-
ports: { shell: shell.transport, connectRuntime: async () => runtime, actions },
|
|
2065
|
-
});
|
|
2066
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2067
|
-
await settle();
|
|
2068
|
-
|
|
2069
|
-
expect(await client.fitViewport(140, 46)).toBe("geometry-authority-conflict");
|
|
2070
|
-
expect(runtime.viewportFits).toEqual([{ cols: 140, rows: 46 }]);
|
|
2071
|
-
client.dispose();
|
|
2072
|
-
await settle();
|
|
2073
|
-
});
|
|
2074
|
-
|
|
2075
|
-
it("rejects an authority grant that settles after its exact runtime was replaced", async () => {
|
|
2076
|
-
const shell = shellBroker({ alpha: shellResource("alpha"), beta: shellResource("beta") });
|
|
2077
|
-
const alphaRuntime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2078
|
-
const betaRuntime = new FakeRuntime(BETA_DAEMON.instanceId);
|
|
2079
|
-
const lateAuthority = deferred<void>();
|
|
2080
|
-
alphaRuntime.authorityGate = lateAuthority.promise;
|
|
2081
|
-
const client = createWorkspaceClient({
|
|
2082
|
-
target: target("alpha"),
|
|
2083
|
-
ports: {
|
|
2084
|
-
shell: shell.transport,
|
|
2085
|
-
connectRuntime: async (current) =>
|
|
2086
|
-
current.workspaceName === "alpha" ? alphaRuntime : betaRuntime,
|
|
2087
|
-
actions,
|
|
2088
|
-
},
|
|
2089
|
-
});
|
|
2090
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2091
|
-
await settle();
|
|
2092
|
-
|
|
2093
|
-
const staleGrant = client.requestAuthority("input");
|
|
2094
|
-
client.setTarget(target("beta", BETA_DAEMON));
|
|
2095
|
-
shell.connections[1]!.handlers.onVerifiedOpen();
|
|
2096
|
-
await settle();
|
|
2097
|
-
lateAuthority.resolve();
|
|
2098
|
-
await expect(staleGrant).resolves.toBeNull();
|
|
2099
|
-
await expect(client.requestAuthority("input")).resolves.toMatchObject({
|
|
2100
|
-
generation: BETA_DAEMON.instanceId,
|
|
2101
|
-
authority: "input",
|
|
2102
|
-
});
|
|
2103
|
-
client.dispose();
|
|
2104
|
-
await settle();
|
|
2105
|
-
});
|
|
2106
|
-
|
|
2107
|
-
it("delivers external receipt observations to independent clients without creating local operations", async () => {
|
|
2108
|
-
const clients = [];
|
|
2109
|
-
for (let index = 0; index < 2; index++) {
|
|
2110
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
2111
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2112
|
-
const client = createWorkspaceClient({
|
|
2113
|
-
target: target("alpha"),
|
|
2114
|
-
ports: { shell: shell.transport, connectRuntime: async () => runtime, actions },
|
|
2115
|
-
});
|
|
2116
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2117
|
-
await settle();
|
|
2118
|
-
clients.push({ client, runtime });
|
|
2119
|
-
}
|
|
2120
|
-
const external = {
|
|
2121
|
-
...receipt("15000000-0000-4000-8000-000000000015", "observed"),
|
|
2122
|
-
origin: "external" as const,
|
|
2123
|
-
};
|
|
2124
|
-
for (const { client, runtime } of clients) {
|
|
2125
|
-
let publications = 0;
|
|
2126
|
-
client.subscribe("operations", () => publications++);
|
|
2127
|
-
runtime.emitReceipt(external);
|
|
2128
|
-
const observed = publications;
|
|
2129
|
-
runtime.emitReceipt(external);
|
|
2130
|
-
expect(publications).toBe(observed);
|
|
2131
|
-
expect(client.getSnapshot().operations).toMatchObject({
|
|
2132
|
-
lastObservedReceipt: external,
|
|
2133
|
-
pending: [],
|
|
2134
|
-
lastReceipt: null,
|
|
2135
|
-
});
|
|
2136
|
-
client.dispose();
|
|
2137
|
-
}
|
|
2138
|
-
await settle();
|
|
2139
|
-
});
|
|
2140
|
-
|
|
2141
|
-
it("settles duplicate and reordered receipts once without waking semantic subscribers", async () => {
|
|
2142
|
-
const clock = new FakeClock();
|
|
2143
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
2144
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2145
|
-
const operationId = "10000000-0000-4000-8000-000000000001";
|
|
2146
|
-
const client = createWorkspaceClient({
|
|
2147
|
-
target: target("alpha"),
|
|
2148
|
-
ports: { shell: shell.transport, connectRuntime: async () => runtime, actions },
|
|
2149
|
-
clock,
|
|
2150
|
-
operationId: () => operationId,
|
|
2151
|
-
operationTimeoutMs: 50,
|
|
2152
|
-
});
|
|
2153
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2154
|
-
await settle();
|
|
2155
|
-
let semanticPublications = 0;
|
|
2156
|
-
let operationPublications = 0;
|
|
2157
|
-
client.subscribe("semantic", () => (semanticPublications += 1));
|
|
2158
|
-
client.subscribe("operations", () => (operationPublications += 1));
|
|
2159
|
-
const baselineSemantic = semanticPublications;
|
|
2160
|
-
await client.dispatch({
|
|
2161
|
-
kind: "semantic-intent",
|
|
2162
|
-
operationId,
|
|
2163
|
-
intent: {
|
|
2164
|
-
verb: "workspace.pane.read",
|
|
2165
|
-
workspaceName: "alpha",
|
|
2166
|
-
semanticPaneId: "pane.alpha",
|
|
2167
|
-
origin: "sdk",
|
|
2168
|
-
},
|
|
2169
|
-
});
|
|
2170
|
-
runtime.emitReceipt(receipt(operationId, "accepted"));
|
|
2171
|
-
runtime.emitReceipt(receipt(operationId, "accepted"));
|
|
2172
|
-
runtime.emitReceipt(receipt(operationId, "observed"));
|
|
2173
|
-
runtime.emitReceipt(receipt(operationId, "observed"));
|
|
2174
|
-
const afterTerminal = operationPublications;
|
|
2175
|
-
clock.advance(1_000);
|
|
2176
|
-
|
|
2177
|
-
expect(client.getSnapshot().operations.pending).toEqual([]);
|
|
2178
|
-
expect(client.getSnapshot().operations.terminalOperationIds).toEqual([operationId]);
|
|
2179
|
-
expect(operationPublications).toBe(afterTerminal);
|
|
2180
|
-
expect(semanticPublications).toBe(baselineSemantic);
|
|
2181
|
-
client.dispose();
|
|
2182
|
-
client.dispose();
|
|
2183
|
-
await settle();
|
|
2184
|
-
expect(runtime.closeCount).toBe(1);
|
|
2185
|
-
});
|
|
2186
|
-
|
|
2187
|
-
it("rejects pending and terminal duplicate operation ids before semantic transport", async () => {
|
|
2188
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
2189
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2190
|
-
const operationId = "11000000-0000-4000-8000-000000000011";
|
|
2191
|
-
let resolveSubmit!: () => void;
|
|
2192
|
-
const submitCompletion = new Promise<void>((resolve) => (resolveSubmit = resolve));
|
|
2193
|
-
runtime.submitIntent = async () => {
|
|
2194
|
-
runtime.submitCount += 1;
|
|
2195
|
-
await submitCompletion;
|
|
2196
|
-
};
|
|
2197
|
-
const client = createWorkspaceClient({
|
|
2198
|
-
target: target("alpha"),
|
|
2199
|
-
ports: { shell: shell.transport, connectRuntime: async () => runtime, actions },
|
|
2200
|
-
});
|
|
2201
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2202
|
-
await settle();
|
|
2203
|
-
const command = {
|
|
2204
|
-
kind: "semantic-intent" as const,
|
|
2205
|
-
operationId,
|
|
2206
|
-
intent: {
|
|
2207
|
-
verb: "workspace.pane.read" as const,
|
|
2208
|
-
workspaceName: "alpha",
|
|
2209
|
-
semanticPaneId: "pane.alpha",
|
|
2210
|
-
origin: "sdk" as const,
|
|
2211
|
-
},
|
|
2212
|
-
};
|
|
2213
|
-
|
|
2214
|
-
const first = client.dispatch(command);
|
|
2215
|
-
await settle();
|
|
2216
|
-
await expect(client.dispatch(command)).rejects.toThrow("already pending or terminal");
|
|
2217
|
-
expect(runtime.submitCount).toBe(1);
|
|
2218
|
-
resolveSubmit();
|
|
2219
|
-
await first;
|
|
2220
|
-
runtime.emitReceipt(receipt(operationId, "observed"));
|
|
2221
|
-
await expect(client.dispatch(command)).rejects.toThrow("already pending or terminal");
|
|
2222
|
-
expect(runtime.submitCount).toBe(1);
|
|
2223
|
-
client.dispose();
|
|
2224
|
-
await settle();
|
|
2225
|
-
});
|
|
2226
|
-
|
|
2227
|
-
it("rejects pending and terminal duplicate operation ids before owner transport", async () => {
|
|
2228
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
2229
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2230
|
-
const operationId = "12000000-0000-4000-8000-000000000012";
|
|
2231
|
-
let resolveOwner!: (value: null) => void;
|
|
2232
|
-
const ownerCompletion = new Promise<null>((resolve) => (resolveOwner = resolve));
|
|
2233
|
-
let ownerSubmitCount = 0;
|
|
2234
|
-
const client = createWorkspaceClient({
|
|
2235
|
-
target: target("alpha"),
|
|
2236
|
-
ports: {
|
|
2237
|
-
shell: shell.transport,
|
|
2238
|
-
connectRuntime: async () => runtime,
|
|
2239
|
-
actions: {
|
|
2240
|
-
async dispatch() {
|
|
2241
|
-
ownerSubmitCount += 1;
|
|
2242
|
-
return ownerCompletion;
|
|
2243
|
-
},
|
|
2244
|
-
},
|
|
2245
|
-
},
|
|
2246
|
-
});
|
|
2247
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2248
|
-
await settle();
|
|
2249
|
-
const command = {
|
|
2250
|
-
kind: "owner-action" as const,
|
|
2251
|
-
operationId,
|
|
2252
|
-
name: "workspace.pane.select" as const,
|
|
2253
|
-
input: { semanticPaneId: "pane.alpha" },
|
|
2254
|
-
};
|
|
2255
|
-
|
|
2256
|
-
const first = client.dispatch(command);
|
|
2257
|
-
await settle();
|
|
2258
|
-
await expect(client.dispatch(command)).rejects.toThrow("already pending or terminal");
|
|
2259
|
-
expect(ownerSubmitCount).toBe(1);
|
|
2260
|
-
resolveOwner(null);
|
|
2261
|
-
await first;
|
|
2262
|
-
await expect(client.dispatch(command)).rejects.toThrow("already pending or terminal");
|
|
2263
|
-
expect(ownerSubmitCount).toBe(1);
|
|
2264
|
-
client.dispose();
|
|
2265
|
-
await settle();
|
|
2266
|
-
});
|
|
2267
|
-
|
|
2268
|
-
it("retires pane publications, receipts, and operation timers with a target generation", async () => {
|
|
2269
|
-
const timerBaseline = runtimeResourceSnapshot()["runtime-timer"].active;
|
|
2270
|
-
const clock = new FakeClock();
|
|
2271
|
-
const shell = shellBroker({ alpha: shellResource("alpha"), beta: shellResource("beta") });
|
|
2272
|
-
const alphaRuntime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2273
|
-
const betaRuntime = new FakeRuntime(BETA_DAEMON.instanceId);
|
|
2274
|
-
const operationId = "10000000-0000-4000-8000-000000000001";
|
|
2275
|
-
const client = createWorkspaceClient({
|
|
2276
|
-
target: target("alpha"),
|
|
2277
|
-
ports: {
|
|
2278
|
-
shell: shell.transport,
|
|
2279
|
-
connectRuntime: async (current) =>
|
|
2280
|
-
current.workspaceName === "alpha" ? alphaRuntime : betaRuntime,
|
|
2281
|
-
actions,
|
|
2282
|
-
},
|
|
2283
|
-
clock,
|
|
2284
|
-
operationTimeoutMs: 50,
|
|
2285
|
-
});
|
|
2286
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2287
|
-
let terminalWakeups = 0;
|
|
2288
|
-
client.subscribeTerminal(
|
|
2289
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.alpha" },
|
|
2290
|
-
() => (terminalWakeups += 1),
|
|
2291
|
-
);
|
|
2292
|
-
await settle();
|
|
2293
|
-
await client.dispatch({
|
|
2294
|
-
kind: "semantic-intent",
|
|
2295
|
-
operationId,
|
|
2296
|
-
intent: {
|
|
2297
|
-
verb: "workspace.pane.read",
|
|
2298
|
-
workspaceName: "alpha",
|
|
2299
|
-
semanticPaneId: "pane.alpha",
|
|
2300
|
-
origin: "sdk",
|
|
2301
|
-
},
|
|
2302
|
-
});
|
|
2303
|
-
const retiredSubscription = alphaRuntime.subscriptions.get("pane.alpha")!;
|
|
2304
|
-
expect(runtimeResourceSnapshot()["runtime-timer"].active).toBeGreaterThan(timerBaseline);
|
|
2305
|
-
|
|
2306
|
-
client.setTarget(target("beta", BETA_DAEMON));
|
|
2307
|
-
retiredSubscription.emit(terminalSeed(ALPHA_DAEMON.instanceId));
|
|
2308
|
-
alphaRuntime.emitReceipt(receipt(operationId, "observed"));
|
|
2309
|
-
clock.advance(1_000);
|
|
2310
|
-
await settle();
|
|
2311
|
-
|
|
2312
|
-
expect(terminalWakeups).toBe(0);
|
|
2313
|
-
expect(client.getSnapshot().generation).toBe(2);
|
|
2314
|
-
expect(client.getSnapshot().operations).toMatchObject({
|
|
2315
|
-
pending: [],
|
|
2316
|
-
terminalOperationIds: [],
|
|
2317
|
-
lastReceipt: null,
|
|
2318
|
-
});
|
|
2319
|
-
expect(retiredSubscription.closeCount).toBe(1);
|
|
2320
|
-
expect(runtimeResourceSnapshot()["runtime-timer"].active).toBe(timerBaseline);
|
|
2321
|
-
client.dispose();
|
|
2322
|
-
await settle();
|
|
2323
|
-
});
|
|
2324
|
-
|
|
2325
|
-
it("keeps pane delivery scoped and bounded across 2, 4, and 8 clients", async () => {
|
|
2326
|
-
for (const count of [2, 4, 8]) {
|
|
2327
|
-
const clients = [];
|
|
2328
|
-
const runtimes: FakeRuntime[] = [];
|
|
2329
|
-
let semanticWakeups = 0;
|
|
2330
|
-
let terminalWakeups = 0;
|
|
2331
|
-
for (let index = 0; index < count; index += 1) {
|
|
2332
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
2333
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2334
|
-
const client = createWorkspaceClient({
|
|
2335
|
-
target: target("alpha"),
|
|
2336
|
-
ports: { shell: shell.transport, connectRuntime: async () => runtime, actions },
|
|
2337
|
-
});
|
|
2338
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2339
|
-
client.subscribe("semantic", () => (semanticWakeups += 1));
|
|
2340
|
-
client.subscribeTerminal(
|
|
2341
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.alpha" },
|
|
2342
|
-
() => (terminalWakeups += 1),
|
|
2343
|
-
);
|
|
2344
|
-
clients.push(client);
|
|
2345
|
-
runtimes.push(runtime);
|
|
2346
|
-
}
|
|
2347
|
-
await settle();
|
|
2348
|
-
const semanticBaseline = semanticWakeups;
|
|
2349
|
-
for (const runtime of runtimes) {
|
|
2350
|
-
runtime.subscriptions.get("pane.alpha")!.emit(terminalSeed(ALPHA_DAEMON.instanceId));
|
|
2351
|
-
}
|
|
2352
|
-
expect(terminalWakeups).toBe(count);
|
|
2353
|
-
expect(semanticWakeups).toBe(semanticBaseline);
|
|
2354
|
-
expect(runtimes.reduce((sum, runtime) => sum + runtime.subscriptions.size, 0)).toBe(count);
|
|
2355
|
-
for (const client of clients) client.dispose();
|
|
2356
|
-
await settle();
|
|
2357
|
-
expect(runtimes.every((runtime) => runtime.closeCount === 1)).toBe(true);
|
|
2358
|
-
}
|
|
2359
|
-
});
|
|
2360
|
-
|
|
2361
|
-
it("keeps workspace prepare and decision in one generation-fenced action lifecycle", async () => {
|
|
2362
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
2363
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2364
|
-
const prepareToken = "30000000-0000-4000-8000-000000000003";
|
|
2365
|
-
const calls: string[] = [];
|
|
2366
|
-
const handoffActions: WorkspaceClientOwnerActionPort = {
|
|
2367
|
-
async dispatch(input) {
|
|
2368
|
-
calls.push(input.name);
|
|
2369
|
-
if (input.name === "workspace.open.prepare") {
|
|
2370
|
-
return {
|
|
2371
|
-
operationId: "20000000-0000-4000-8000-000000000002",
|
|
2372
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
2373
|
-
phase: "prepared",
|
|
2374
|
-
prepareToken,
|
|
2375
|
-
preparedRevision: 1,
|
|
2376
|
-
outcome: "reopened",
|
|
2377
|
-
workspaceName: "alpha",
|
|
2378
|
-
previousWorkspaceName: null,
|
|
2379
|
-
proof: {
|
|
2380
|
-
semanticPaneId: "pane.alpha",
|
|
2381
|
-
paneCount: 1,
|
|
2382
|
-
terminalRevision: 0,
|
|
2383
|
-
terminalStateHash: "0000000000000000",
|
|
2384
|
-
},
|
|
2385
|
-
} as never;
|
|
2386
|
-
}
|
|
2387
|
-
return {
|
|
2388
|
-
operationId: "40000000-0000-4000-8000-000000000004",
|
|
2389
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
2390
|
-
phase: "committed",
|
|
2391
|
-
prepareToken,
|
|
2392
|
-
preparedRevision: 1,
|
|
2393
|
-
workspaceName: "alpha",
|
|
2394
|
-
previousWorkspaceName: null,
|
|
2395
|
-
} as never;
|
|
2396
|
-
},
|
|
2397
|
-
};
|
|
2398
|
-
let nextOperation = 10;
|
|
2399
|
-
const client = createWorkspaceClient({
|
|
2400
|
-
target: target("alpha"),
|
|
2401
|
-
ports: {
|
|
2402
|
-
shell: shell.transport,
|
|
2403
|
-
connectRuntime: async () => runtime,
|
|
2404
|
-
actions: handoffActions,
|
|
2405
|
-
},
|
|
2406
|
-
operationId: () => `50000000-0000-4000-8000-${String(nextOperation++).padStart(12, "0")}`,
|
|
2407
|
-
});
|
|
2408
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2409
|
-
await settle();
|
|
2410
|
-
await client.dispatch({
|
|
2411
|
-
kind: "owner-action",
|
|
2412
|
-
name: "workspace.open.prepare",
|
|
2413
|
-
input: { source: { kind: "project", projectDir: "/project" } },
|
|
2414
|
-
});
|
|
2415
|
-
await expect(
|
|
2416
|
-
client.dispatch({
|
|
2417
|
-
kind: "owner-action",
|
|
2418
|
-
name: "workspace.open.commit",
|
|
2419
|
-
input: {
|
|
2420
|
-
prepareToken: "60000000-0000-4000-8000-000000000006",
|
|
2421
|
-
preparedRevision: 1,
|
|
2422
|
-
},
|
|
2423
|
-
}),
|
|
2424
|
-
).rejects.toThrow("does not match");
|
|
2425
|
-
await client.dispatch({
|
|
2426
|
-
kind: "owner-action",
|
|
2427
|
-
name: "workspace.open.commit",
|
|
2428
|
-
input: { prepareToken, preparedRevision: 1 },
|
|
2429
|
-
});
|
|
2430
|
-
expect(calls).toEqual(["workspace.open.prepare", "workspace.open.commit"]);
|
|
2431
|
-
client.dispose();
|
|
2432
|
-
await settle();
|
|
2433
|
-
});
|
|
2434
|
-
|
|
2435
|
-
it("terminalizes a rejected owner action immediately", async () => {
|
|
2436
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
2437
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2438
|
-
const operationId = "70000000-0000-4000-8000-000000000007";
|
|
2439
|
-
const client = createWorkspaceClient({
|
|
2440
|
-
target: target("alpha"),
|
|
2441
|
-
ports: {
|
|
2442
|
-
shell: shell.transport,
|
|
2443
|
-
connectRuntime: async () => runtime,
|
|
2444
|
-
actions: {
|
|
2445
|
-
dispatch: async () => {
|
|
2446
|
-
throw new Error("owner rejected");
|
|
2447
|
-
},
|
|
2448
|
-
},
|
|
2449
|
-
},
|
|
2450
|
-
});
|
|
2451
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2452
|
-
await settle();
|
|
2453
|
-
|
|
2454
|
-
await expect(
|
|
2455
|
-
client.dispatch({
|
|
2456
|
-
kind: "owner-action",
|
|
2457
|
-
operationId,
|
|
2458
|
-
name: "workspace.pane.select",
|
|
2459
|
-
input: { semanticPaneId: "pane.alpha" },
|
|
2460
|
-
}),
|
|
2461
|
-
).rejects.toThrow("owner rejected");
|
|
2462
|
-
|
|
2463
|
-
expect(client.getSnapshot().operations.pending).toEqual([]);
|
|
2464
|
-
expect(client.getSnapshot().operations.terminalOperationIds).toContain(operationId);
|
|
2465
|
-
client.dispose();
|
|
2466
|
-
await settle();
|
|
2467
|
-
});
|
|
2468
|
-
|
|
2469
|
-
it("actively cancels a prepared open on target replacement and fences its late completion", async () => {
|
|
2470
|
-
const shell = shellBroker({ alpha: shellResource("alpha"), beta: shellResource("beta") });
|
|
2471
|
-
const alphaRuntime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2472
|
-
const betaRuntime = new FakeRuntime(BETA_DAEMON.instanceId);
|
|
2473
|
-
const prepareToken = "80000000-0000-4000-8000-000000000008";
|
|
2474
|
-
let rejectCancel!: (error: Error) => void;
|
|
2475
|
-
const cancelCompletion = new Promise<void>((_resolve, reject) => (rejectCancel = reject));
|
|
2476
|
-
const calls: Array<{ name: string; workspaceName: string; input: unknown }> = [];
|
|
2477
|
-
const client = createWorkspaceClient({
|
|
2478
|
-
target: target("alpha"),
|
|
2479
|
-
ports: {
|
|
2480
|
-
shell: shell.transport,
|
|
2481
|
-
connectRuntime: async (current) =>
|
|
2482
|
-
current.workspaceName === "alpha" ? alphaRuntime : betaRuntime,
|
|
2483
|
-
actions: {
|
|
2484
|
-
async dispatch(input) {
|
|
2485
|
-
calls.push({
|
|
2486
|
-
name: input.name,
|
|
2487
|
-
workspaceName: input.target.workspaceName,
|
|
2488
|
-
input: input.input,
|
|
2489
|
-
});
|
|
2490
|
-
if (input.name === "workspace.open.prepare") {
|
|
2491
|
-
return {
|
|
2492
|
-
operationId: input.operationId,
|
|
2493
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
2494
|
-
phase: "prepared",
|
|
2495
|
-
prepareToken,
|
|
2496
|
-
preparedRevision: 4,
|
|
2497
|
-
outcome: "reopened",
|
|
2498
|
-
workspaceName: "alpha",
|
|
2499
|
-
previousWorkspaceName: null,
|
|
2500
|
-
proof: {
|
|
2501
|
-
semanticPaneId: "pane.alpha",
|
|
2502
|
-
paneCount: 1,
|
|
2503
|
-
terminalRevision: 0,
|
|
2504
|
-
terminalStateHash: "0000000000000000",
|
|
2505
|
-
},
|
|
2506
|
-
} as never;
|
|
2507
|
-
}
|
|
2508
|
-
if (input.name === "workspace.open.cancel") await cancelCompletion;
|
|
2509
|
-
return null as never;
|
|
2510
|
-
},
|
|
2511
|
-
},
|
|
2512
|
-
},
|
|
2513
|
-
});
|
|
2514
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2515
|
-
await settle();
|
|
2516
|
-
await client.dispatch({
|
|
2517
|
-
kind: "owner-action",
|
|
2518
|
-
name: "workspace.open.prepare",
|
|
2519
|
-
input: { source: { kind: "project", projectDir: "/project" } },
|
|
2520
|
-
});
|
|
2521
|
-
|
|
2522
|
-
client.setTarget(target("beta", BETA_DAEMON));
|
|
2523
|
-
await settle();
|
|
2524
|
-
expect(calls).toContainEqual({
|
|
2525
|
-
name: "workspace.open.cancel",
|
|
2526
|
-
workspaceName: "alpha",
|
|
2527
|
-
input: { prepareToken, preparedRevision: 4 },
|
|
2528
|
-
});
|
|
2529
|
-
expect(client.getSnapshot().generation).toBe(2);
|
|
2530
|
-
expect(client.getSnapshot().operations.pending).toEqual([]);
|
|
2531
|
-
|
|
2532
|
-
rejectCancel(new Error("late retired cancellation failure"));
|
|
2533
|
-
await settle();
|
|
2534
|
-
expect(client.getSnapshot().generation).toBe(2);
|
|
2535
|
-
expect(client.getSnapshot().target?.workspaceName).toBe("beta");
|
|
2536
|
-
expect(client.getSnapshot().operations.pending).toEqual([]);
|
|
2537
|
-
client.dispose();
|
|
2538
|
-
await settle();
|
|
2539
|
-
});
|
|
2540
|
-
|
|
2541
|
-
it("cancels a capability whose prepare resolves after its exact target retired", async () => {
|
|
2542
|
-
const shell = shellBroker({ alpha: shellResource("alpha"), beta: shellResource("beta") });
|
|
2543
|
-
const pending = deferred<unknown>();
|
|
2544
|
-
const calls: Array<{ name: string; workspaceName: string; input: unknown }> = [];
|
|
2545
|
-
const prepareToken = "81000000-0000-4000-8000-000000000008";
|
|
2546
|
-
const client = createWorkspaceClient({
|
|
2547
|
-
target: target("alpha"),
|
|
2548
|
-
ports: {
|
|
2549
|
-
shell: shell.transport,
|
|
2550
|
-
connectRuntime: async (current) => new FakeRuntime(current.daemon.instanceId),
|
|
2551
|
-
actions: {
|
|
2552
|
-
async dispatch(input) {
|
|
2553
|
-
calls.push({
|
|
2554
|
-
name: input.name,
|
|
2555
|
-
workspaceName: input.target.workspaceName,
|
|
2556
|
-
input: input.input,
|
|
2557
|
-
});
|
|
2558
|
-
if (input.name === "workspace.open.prepare") return (await pending.promise) as never;
|
|
2559
|
-
return null as never;
|
|
2560
|
-
},
|
|
2561
|
-
},
|
|
2562
|
-
},
|
|
2563
|
-
});
|
|
2564
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2565
|
-
await settle();
|
|
2566
|
-
const preparing = client.dispatch({
|
|
2567
|
-
kind: "owner-action",
|
|
2568
|
-
name: "workspace.open.prepare",
|
|
2569
|
-
input: { source: { kind: "project", projectDir: "/project" } },
|
|
2570
|
-
});
|
|
2571
|
-
client.setTarget(target("beta", BETA_DAEMON));
|
|
2572
|
-
pending.resolve({
|
|
2573
|
-
operationId: "late-prepare",
|
|
2574
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
2575
|
-
phase: "prepared",
|
|
2576
|
-
prepareToken,
|
|
2577
|
-
preparedRevision: 7,
|
|
2578
|
-
outcome: "reopened",
|
|
2579
|
-
workspaceName: "alpha",
|
|
2580
|
-
previousWorkspaceName: null,
|
|
2581
|
-
proof: {
|
|
2582
|
-
semanticPaneId: "pane.alpha",
|
|
2583
|
-
paneCount: 1,
|
|
2584
|
-
terminalRevision: 0,
|
|
2585
|
-
terminalStateHash: "0000000000000000",
|
|
2586
|
-
},
|
|
2587
|
-
});
|
|
2588
|
-
await expect(preparing).rejects.toThrow("retired");
|
|
2589
|
-
expect(calls.at(-1)).toEqual({
|
|
2590
|
-
name: "workspace.open.cancel",
|
|
2591
|
-
workspaceName: "alpha",
|
|
2592
|
-
input: { prepareToken, preparedRevision: 7 },
|
|
2593
|
-
});
|
|
2594
|
-
await client.dispose();
|
|
2595
|
-
});
|
|
2596
|
-
|
|
2597
|
-
it("actively cancels a prepared open during idempotent disposal", async () => {
|
|
2598
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
2599
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2600
|
-
const prepareToken = "90000000-0000-4000-8000-000000000009";
|
|
2601
|
-
const calls: string[] = [];
|
|
2602
|
-
const client = createWorkspaceClient({
|
|
2603
|
-
target: target("alpha"),
|
|
2604
|
-
ports: {
|
|
2605
|
-
shell: shell.transport,
|
|
2606
|
-
connectRuntime: async () => runtime,
|
|
2607
|
-
actions: {
|
|
2608
|
-
async dispatch(input) {
|
|
2609
|
-
calls.push(input.name);
|
|
2610
|
-
if (input.name === "workspace.open.prepare") {
|
|
2611
|
-
return {
|
|
2612
|
-
operationId: input.operationId,
|
|
2613
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
2614
|
-
phase: "prepared",
|
|
2615
|
-
prepareToken,
|
|
2616
|
-
preparedRevision: 2,
|
|
2617
|
-
outcome: "reopened",
|
|
2618
|
-
workspaceName: "alpha",
|
|
2619
|
-
previousWorkspaceName: null,
|
|
2620
|
-
proof: {
|
|
2621
|
-
semanticPaneId: "pane.alpha",
|
|
2622
|
-
paneCount: 1,
|
|
2623
|
-
terminalRevision: 0,
|
|
2624
|
-
terminalStateHash: "0000000000000000",
|
|
2625
|
-
},
|
|
2626
|
-
} as never;
|
|
2627
|
-
}
|
|
2628
|
-
return null as never;
|
|
2629
|
-
},
|
|
2630
|
-
},
|
|
2631
|
-
},
|
|
2632
|
-
});
|
|
2633
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2634
|
-
await settle();
|
|
2635
|
-
await client.dispatch({
|
|
2636
|
-
kind: "owner-action",
|
|
2637
|
-
name: "workspace.open.prepare",
|
|
2638
|
-
input: { source: { kind: "project", projectDir: "/project" } },
|
|
2639
|
-
});
|
|
2640
|
-
|
|
2641
|
-
await client.dispose();
|
|
2642
|
-
await client.dispose();
|
|
2643
|
-
await settle();
|
|
2644
|
-
expect(calls).toEqual(["workspace.open.prepare", "workspace.open.cancel"]);
|
|
2645
|
-
expect(client.getSnapshot().phase).toBe("disposed");
|
|
2646
|
-
expect(runtime.closeCount).toBe(1);
|
|
2647
|
-
});
|
|
2648
|
-
|
|
2649
|
-
it("cancels a capability whose prepare resolves after disposal", async () => {
|
|
2650
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
2651
|
-
const pending = deferred<unknown>();
|
|
2652
|
-
const calls: string[] = [];
|
|
2653
|
-
const prepareToken = "91000000-0000-4000-8000-000000000009";
|
|
2654
|
-
const client = createWorkspaceClient({
|
|
2655
|
-
target: target("alpha"),
|
|
2656
|
-
ports: {
|
|
2657
|
-
shell: shell.transport,
|
|
2658
|
-
connectRuntime: async () => new FakeRuntime(ALPHA_DAEMON.instanceId),
|
|
2659
|
-
actions: {
|
|
2660
|
-
async dispatch(input) {
|
|
2661
|
-
calls.push(input.name);
|
|
2662
|
-
if (input.name === "workspace.open.prepare") return (await pending.promise) as never;
|
|
2663
|
-
return null as never;
|
|
2664
|
-
},
|
|
2665
|
-
},
|
|
2666
|
-
},
|
|
2667
|
-
});
|
|
2668
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2669
|
-
await settle();
|
|
2670
|
-
const preparing = client.dispatch({
|
|
2671
|
-
kind: "owner-action",
|
|
2672
|
-
name: "workspace.open.prepare",
|
|
2673
|
-
input: { source: { kind: "project", projectDir: "/project" } },
|
|
2674
|
-
});
|
|
2675
|
-
const disposal = client.dispose();
|
|
2676
|
-
pending.resolve({
|
|
2677
|
-
operationId: "late-disposed-prepare",
|
|
2678
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
2679
|
-
phase: "prepared",
|
|
2680
|
-
prepareToken,
|
|
2681
|
-
preparedRevision: 8,
|
|
2682
|
-
outcome: "reopened",
|
|
2683
|
-
workspaceName: "alpha",
|
|
2684
|
-
previousWorkspaceName: null,
|
|
2685
|
-
proof: {
|
|
2686
|
-
semanticPaneId: "pane.alpha",
|
|
2687
|
-
paneCount: 1,
|
|
2688
|
-
terminalRevision: 0,
|
|
2689
|
-
terminalStateHash: "0000000000000000",
|
|
2690
|
-
},
|
|
2691
|
-
});
|
|
2692
|
-
await expect(preparing).rejects.toThrow("retired");
|
|
2693
|
-
await disposal;
|
|
2694
|
-
expect(calls).toEqual(["workspace.open.prepare", "workspace.open.cancel"]);
|
|
2695
|
-
});
|
|
2696
|
-
|
|
2697
|
-
it("does not settle disposal until the runtime supervisor closes its transport", async () => {
|
|
2698
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
2699
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2700
|
-
const closing = deferred<void>();
|
|
2701
|
-
runtime.closeGate = closing.promise;
|
|
2702
|
-
const client = createWorkspaceClient({
|
|
2703
|
-
target: target("alpha"),
|
|
2704
|
-
ports: {
|
|
2705
|
-
shell: shell.transport,
|
|
2706
|
-
connectRuntime: async () => runtime,
|
|
2707
|
-
actions,
|
|
2708
|
-
},
|
|
2709
|
-
});
|
|
2710
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2711
|
-
await settle();
|
|
2712
|
-
|
|
2713
|
-
let settled = false;
|
|
2714
|
-
const disposing = client.dispose().then(() => (settled = true));
|
|
2715
|
-
await settle();
|
|
2716
|
-
expect(runtime.closeCount).toBe(1);
|
|
2717
|
-
expect(settled).toBe(false);
|
|
2718
|
-
closing.resolve();
|
|
2719
|
-
await disposing;
|
|
2720
|
-
expect(settled).toBe(true);
|
|
2721
|
-
});
|
|
2722
|
-
|
|
2723
|
-
it("does not settle target retirement until its terminal subscription closes", async () => {
|
|
2724
|
-
const shell = shellBroker({ alpha: shellResource("alpha"), beta: shellResource("beta") });
|
|
2725
|
-
const alpha = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2726
|
-
const beta = new FakeRuntime(BETA_DAEMON.instanceId);
|
|
2727
|
-
const client = createWorkspaceClient({
|
|
2728
|
-
target: target("alpha"),
|
|
2729
|
-
ports: {
|
|
2730
|
-
shell: shell.transport,
|
|
2731
|
-
connectRuntime: async (current) => (current.workspaceName === "alpha" ? alpha : beta),
|
|
2732
|
-
actions,
|
|
2733
|
-
},
|
|
2734
|
-
});
|
|
2735
|
-
client.subscribeTerminal(
|
|
2736
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.alpha" },
|
|
2737
|
-
() => undefined,
|
|
2738
|
-
);
|
|
2739
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2740
|
-
await settle();
|
|
2741
|
-
const subscription = alpha.subscriptions.get("pane.alpha")!;
|
|
2742
|
-
const closeGate = deferred<void>();
|
|
2743
|
-
subscription.closeGate = closeGate.promise;
|
|
2744
|
-
|
|
2745
|
-
let retired = false;
|
|
2746
|
-
const retirement = client.setTarget(target("beta", BETA_DAEMON)).then(() => (retired = true));
|
|
2747
|
-
await settle();
|
|
2748
|
-
expect(subscription.closeCount).toBe(1);
|
|
2749
|
-
expect(retired).toBe(false);
|
|
2750
|
-
closeGate.resolve();
|
|
2751
|
-
await retirement;
|
|
2752
|
-
expect(retired).toBe(true);
|
|
2753
|
-
await client.dispose();
|
|
2754
|
-
});
|
|
2755
|
-
|
|
2756
|
-
it("fences and closes a terminal subscription that resolves after disposal settled", async () => {
|
|
2757
|
-
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
2758
|
-
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2759
|
-
const subscriptionStarted = deferred<void>();
|
|
2760
|
-
const subscriptionReady = deferred<FakeTerminalSubscription>();
|
|
2761
|
-
runtime.subscribeTerminal = async () => {
|
|
2762
|
-
subscriptionStarted.resolve();
|
|
2763
|
-
return subscriptionReady.promise;
|
|
2764
|
-
};
|
|
2765
|
-
const client = createWorkspaceClient({
|
|
2766
|
-
target: target("alpha"),
|
|
2767
|
-
ports: { shell: shell.transport, connectRuntime: async () => runtime, actions },
|
|
2768
|
-
});
|
|
2769
|
-
client.subscribeTerminal(
|
|
2770
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.alpha" },
|
|
2771
|
-
() => undefined,
|
|
2772
|
-
);
|
|
2773
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2774
|
-
await subscriptionStarted.promise;
|
|
2775
|
-
|
|
2776
|
-
await client.dispose();
|
|
2777
|
-
expect(client.getSnapshot().phase).toBe("disposed");
|
|
2778
|
-
|
|
2779
|
-
const closeStarted = deferred<void>();
|
|
2780
|
-
const closeFinished = deferred<void>();
|
|
2781
|
-
const late = new FakeTerminalSubscription(ALPHA_DAEMON.instanceId);
|
|
2782
|
-
const closeGate = deferred<void>();
|
|
2783
|
-
late.close = async () => {
|
|
2784
|
-
late.closeCount += 1;
|
|
2785
|
-
closeStarted.resolve();
|
|
2786
|
-
await closeGate.promise;
|
|
2787
|
-
closeFinished.resolve();
|
|
2788
|
-
};
|
|
2789
|
-
subscriptionReady.resolve(late);
|
|
2790
|
-
await closeStarted.promise;
|
|
2791
|
-
expect(late.closeCount).toBe(1);
|
|
2792
|
-
closeGate.resolve();
|
|
2793
|
-
await closeFinished.promise;
|
|
2794
|
-
expect(late.closeCount).toBe(1);
|
|
2795
|
-
});
|
|
2796
|
-
});
|
|
2797
|
-
|
|
2798
|
-
describe("semantic mutation acknowledgement across topology replacement", () => {
|
|
2799
|
-
const operationId = "45000000-0000-4000-8000-000000000045";
|
|
2800
|
-
const command = {
|
|
2801
|
-
kind: "semantic-intent" as const,
|
|
2802
|
-
operationId,
|
|
2803
|
-
intent: {
|
|
2804
|
-
verb: "workspace.window.split" as const,
|
|
2805
|
-
workspaceName: "alpha",
|
|
2806
|
-
semanticPaneId: "pane.alpha",
|
|
2807
|
-
direction: "right" as const,
|
|
2808
|
-
},
|
|
2809
|
-
};
|
|
2810
|
-
const acknowledgement = {
|
|
2811
|
-
operationId,
|
|
2812
|
-
daemonInstanceId: ALPHA_DAEMON.instanceId,
|
|
2813
|
-
outcome: "applied" as const,
|
|
2814
|
-
workspaceName: "alpha",
|
|
2815
|
-
verb: "workspace.window.split" as const,
|
|
2816
|
-
direction: "right" as const,
|
|
2817
|
-
semanticPaneId: "pane.new",
|
|
2818
|
-
displayTitle: "Shell",
|
|
2819
|
-
};
|
|
2820
|
-
async function fixture(
|
|
2821
|
-
beforeReplacement?: (client: WorkspaceClient<string, string>, active: FakeRuntime) => void,
|
|
2822
|
-
) {
|
|
2823
|
-
const shell = shellBroker({ alpha: shellResource("alpha"), beta: shellResource("beta") });
|
|
2824
|
-
const response = deferred<typeof acknowledgement | void>();
|
|
2825
|
-
const active = Object.assign(new FakeRuntime(ALPHA_DAEMON.instanceId), {
|
|
2826
|
-
submitIntent: () => response.promise,
|
|
2827
|
-
});
|
|
2828
|
-
const replacement = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
2829
|
-
const client = createWorkspaceClient({
|
|
2830
|
-
target: target("alpha"),
|
|
2831
|
-
ports: {
|
|
2832
|
-
shell: shell.transport,
|
|
2833
|
-
connectRuntime: async (_target, inventory) =>
|
|
2834
|
-
inventory.semanticPaneIds.includes("pane.new") ? replacement : active,
|
|
2835
|
-
actions,
|
|
2836
|
-
},
|
|
2837
|
-
});
|
|
2838
|
-
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
2839
|
-
await settle();
|
|
2840
|
-
beforeReplacement?.(client, active);
|
|
2841
|
-
const completion = client.dispatch(command);
|
|
2842
|
-
// Attach rejection observation before advancing asynchronous topology work.
|
|
2843
|
-
void completion.catch(() => undefined);
|
|
2844
|
-
shell.byWorkspace.set("alpha", shellResource("alpha", ["pane.alpha", "pane.new"]));
|
|
2845
|
-
shell.connections[0]!.handlers.onInvalidate();
|
|
2846
|
-
await settle();
|
|
2847
|
-
expect(active.closeCount).toBe(1);
|
|
2848
|
-
return { client, response, completion };
|
|
2849
|
-
}
|
|
2850
|
-
it("accepts the exact durable acknowledgement without retaining a stream receipt dependency", async () => {
|
|
2851
|
-
const { client, response, completion } = await fixture();
|
|
2852
|
-
response.resolve(acknowledgement);
|
|
2853
|
-
await expect(completion).resolves.toMatchObject({
|
|
2854
|
-
kind: "semantic-intent",
|
|
2855
|
-
operationId,
|
|
2856
|
-
result: acknowledgement,
|
|
2857
|
-
});
|
|
2858
|
-
await expect(client.dispatch(command)).rejects.toThrow("already pending or terminal");
|
|
2859
|
-
await client.dispose();
|
|
2860
|
-
});
|
|
2861
|
-
it.each([
|
|
2862
|
-
["operation", { operationId: "55000000-0000-4000-8000-000000000055" }],
|
|
2863
|
-
["daemon", { daemonInstanceId: BETA_DAEMON.instanceId }],
|
|
2864
|
-
["workspace", { workspaceName: "beta" }],
|
|
2865
|
-
["verb", { verb: "workspace.session.kill" }],
|
|
2866
|
-
["malformed", { outcome: "unknown" }],
|
|
2867
|
-
])("rejects a mismatched %s acknowledgement", async (_label, patch) => {
|
|
2868
|
-
const { client, response, completion } = await fixture();
|
|
2869
|
-
response.resolve({ ...acknowledgement, ...patch } as typeof acknowledgement);
|
|
2870
|
-
await expect(completion).rejects.toThrow("matching mutation acknowledgement");
|
|
2871
|
-
await client.dispose();
|
|
2872
|
-
});
|
|
2873
|
-
it("keeps input, viewport, and authority completions fenced to the exact old runtime", async () => {
|
|
2874
|
-
const gate = deferred<void>();
|
|
2875
|
-
let input!: Promise<unknown>;
|
|
2876
|
-
let fit!: Promise<unknown>;
|
|
2877
|
-
let grant!: Promise<unknown>;
|
|
2878
|
-
const { client, response, completion } = await fixture((client, active) => {
|
|
2879
|
-
active.inputGate = gate.promise;
|
|
2880
|
-
active.viewportGate = gate.promise;
|
|
2881
|
-
active.authorityGate = gate.promise;
|
|
2882
|
-
input = client.sendTerminalInput(
|
|
2883
|
-
{ workspaceName: "alpha", semanticPaneId: "pane.alpha" },
|
|
2884
|
-
{ type: "text", text: "hello" },
|
|
2885
|
-
);
|
|
2886
|
-
fit = client.fitViewport(100, 30);
|
|
2887
|
-
grant = client.requestAuthority("input");
|
|
2888
|
-
});
|
|
2889
|
-
gate.resolve();
|
|
2890
|
-
response.resolve(acknowledgement);
|
|
2891
|
-
await expect(completion).resolves.toMatchObject({ operationId });
|
|
2892
|
-
await expect(input).resolves.toBe("authority-lost");
|
|
2893
|
-
await expect(fit).resolves.toBe("authority-lost");
|
|
2894
|
-
await expect(grant).resolves.toBeNull();
|
|
2895
|
-
await client.dispose();
|
|
2896
|
-
});
|
|
2897
|
-
it("rejects an unacknowledged retired runtime completion", async () => {
|
|
2898
|
-
const { client, response, completion } = await fixture();
|
|
2899
|
-
response.resolve();
|
|
2900
|
-
await expect(completion).rejects.toThrow("matching mutation acknowledgement");
|
|
2901
|
-
await client.dispose();
|
|
2902
|
-
});
|
|
2903
|
-
it.each(["target", "daemon", "dispose"])("still fences %s retirement", async (kind) => {
|
|
2904
|
-
const { client, response, completion } = await fixture();
|
|
2905
|
-
if (kind === "dispose") await client.dispose();
|
|
2906
|
-
else if (kind === "daemon") client.setTarget(target("alpha", BETA_DAEMON));
|
|
2907
|
-
else client.setTarget(target("beta"));
|
|
2908
|
-
response.resolve(acknowledgement);
|
|
2909
|
-
await expect(completion).rejects.toThrow("client generation was retired");
|
|
2910
|
-
await client.dispose();
|
|
2911
|
-
});
|
|
2912
|
-
});
|