@pi-in-go/pigpen-ahp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CREDITS.md +27 -0
- package/LICENSE +23 -0
- package/README.md +148 -0
- package/extensions/ahp/access_options_test.go +116 -0
- package/extensions/ahp/extension.go +54 -0
- package/extensions/ahp/go.mod +12 -0
- package/extensions/ahp/go.sum +2 -0
- package/extensions/ahp/go.work +8 -0
- package/extensions/ahp/internal/channels/chat.go +110 -0
- package/extensions/ahp/internal/channels/root.go +32 -0
- package/extensions/ahp/internal/channels/session.go +128 -0
- package/extensions/ahp/internal/channels/session_test.go +108 -0
- package/extensions/ahp/internal/compose/compose.go +153 -0
- package/extensions/ahp/internal/compose/compose_test.go +120 -0
- package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
- package/extensions/ahp/internal/host/handshake_test.go +206 -0
- package/extensions/ahp/internal/host/host.go +1305 -0
- package/extensions/ahp/internal/host/reconnect_test.go +197 -0
- package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
- package/extensions/ahp/internal/host/schema_test.go +79 -0
- package/extensions/ahp/internal/host/store.go +291 -0
- package/extensions/ahp/internal/host/store_update_test.go +85 -0
- package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
- package/extensions/ahp/internal/host/surface_test.go +95 -0
- package/extensions/ahp/internal/host/workarounds.go +458 -0
- package/extensions/ahp/internal/host/workarounds_test.go +96 -0
- package/extensions/ahp/internal/live/live.go +328 -0
- package/extensions/ahp/internal/live/live_test.go +91 -0
- package/extensions/ahp/internal/live/store.go +113 -0
- package/extensions/ahp/internal/mapper/activity.go +199 -0
- package/extensions/ahp/internal/mapper/activity_test.go +188 -0
- package/extensions/ahp/internal/mapper/bench_test.go +38 -0
- package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
- package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
- package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
- package/extensions/ahp/internal/mapper/js.go +131 -0
- package/extensions/ahp/internal/mapper/mapper.go +600 -0
- package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
- package/extensions/ahp/internal/mapper/messageinput.go +272 -0
- package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
- package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
- package/extensions/ahp/internal/mapper/title.go +34 -0
- package/extensions/ahp/internal/mapper/usermsg.go +87 -0
- package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
- package/extensions/ahp/internal/pi/backend.go +64 -0
- package/extensions/ahp/internal/pi/catalogue.go +370 -0
- package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
- package/extensions/ahp/internal/pi/chatdriver.go +533 -0
- package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
- package/extensions/ahp/internal/pi/clientactions.go +212 -0
- package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
- package/extensions/ahp/internal/pi/completions.go +230 -0
- package/extensions/ahp/internal/pi/completions_test.go +272 -0
- package/extensions/ahp/internal/pi/config_test.go +269 -0
- package/extensions/ahp/internal/pi/deletesession.go +69 -0
- package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
- package/extensions/ahp/internal/pi/disposal_test.go +362 -0
- package/extensions/ahp/internal/pi/fixture_test.go +308 -0
- package/extensions/ahp/internal/pi/foreign_test.go +122 -0
- package/extensions/ahp/internal/pi/harness_test.go +83 -0
- package/extensions/ahp/internal/pi/history.go +269 -0
- package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
- package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
- package/extensions/ahp/internal/pi/hydration_test.go +209 -0
- package/extensions/ahp/internal/pi/hydrator.go +212 -0
- package/extensions/ahp/internal/pi/imageinput.go +224 -0
- package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
- package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
- package/extensions/ahp/internal/pi/models.go +212 -0
- package/extensions/ahp/internal/pi/models_test.go +128 -0
- package/extensions/ahp/internal/pi/paging.go +95 -0
- package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
- package/extensions/ahp/internal/pi/projecttrust.go +222 -0
- package/extensions/ahp/internal/pi/registry.go +924 -0
- package/extensions/ahp/internal/pi/restart_test.go +185 -0
- package/extensions/ahp/internal/pi/services.go +70 -0
- package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
- package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
- package/extensions/ahp/internal/pi/sessionstore.go +83 -0
- package/extensions/ahp/internal/pi/summary_test.go +426 -0
- package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
- package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
- package/extensions/ahp/internal/pisession/json.go +31 -0
- package/extensions/ahp/internal/pisession/pisession.go +580 -0
- package/extensions/ahp/internal/settings/access.go +63 -0
- package/extensions/ahp/internal/settings/access_test.go +52 -0
- package/extensions/ahp/internal/settings/settings.go +117 -0
- package/extensions/ahp/internal/settings/settings_test.go +104 -0
- package/extensions/ahp/internal/svc/etag_other.go +15 -0
- package/extensions/ahp/internal/svc/etag_unix.go +34 -0
- package/extensions/ahp/internal/svc/glob.go +197 -0
- package/extensions/ahp/internal/svc/mime.go +37 -0
- package/extensions/ahp/internal/svc/paths.go +108 -0
- package/extensions/ahp/internal/svc/pty.go +64 -0
- package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
- package/extensions/ahp/internal/svc/pty_linux.go +36 -0
- package/extensions/ahp/internal/svc/pty_other.go +9 -0
- package/extensions/ahp/internal/svc/pty_test.go +114 -0
- package/extensions/ahp/internal/svc/pty_unix.go +147 -0
- package/extensions/ahp/internal/svc/resource.go +495 -0
- package/extensions/ahp/internal/svc/resource_test.go +501 -0
- package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
- package/extensions/ahp/internal/svc/stat_linux.go +9 -0
- package/extensions/ahp/internal/svc/terminal.go +454 -0
- package/extensions/ahp/internal/svc/terminal_test.go +512 -0
- package/extensions/ahp/internal/svc/watch.go +590 -0
- package/extensions/ahp/internal/svc/watch_test.go +627 -0
- package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
- package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
- package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
- package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
- package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
- package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
- package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
- package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
- package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
- package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
- package/extensions/ahp/internal/testkit/schema.go +360 -0
- package/extensions/ahp/internal/testkit/testkit.go +367 -0
- package/extensions/ahp/internal/twin/twin.go +27 -0
- package/extensions/ahp/internal/wire/dispatchable.go +106 -0
- package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
- package/extensions/ahp/internal/wire/helpers_test.go +5 -0
- package/extensions/ahp/internal/wire/wire.go +218 -0
- package/extensions/ahp/internal/wire/wire_test.go +123 -0
- package/extensions/ahp/internal/ws/helpers_test.go +7 -0
- package/extensions/ahp/internal/ws/vscode_test.go +198 -0
- package/extensions/ahp/internal/ws/ws.go +674 -0
- package/extensions/ahp/internal/ws/ws_test.go +324 -0
- package/extensions/ahp/realpig_test.go +589 -0
- package/extensions/ahp/runtime.go +311 -0
- package/extensions/ahp/runtime_test.go +110 -0
- package/extensions/ahp/testdata/upstream-tests.json +3833 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
- package/extensions/ahp/twins_test.go +91 -0
- package/package.json +40 -0
- package/proof/PORT.md +121 -0
- package/proof/mutations.json +152 -0
- package/proof/oracle/LICENSE +21 -0
- package/proof/oracle/README.md +120 -0
- package/proof/oracle/UPSTREAM.md +7 -0
- package/proof/oracle/package.json +68 -0
- package/proof/oracle/src/bin/cli.ts +66 -0
- package/proof/oracle/src/bin/tunnel.ts +128 -0
- package/proof/oracle/src/channels/chat.ts +134 -0
- package/proof/oracle/src/channels/root.ts +39 -0
- package/proof/oracle/src/channels/session.ts +118 -0
- package/proof/oracle/src/channels/terminal.ts +11 -0
- package/proof/oracle/src/core/channels.ts +108 -0
- package/proof/oracle/src/core/client-workarounds.ts +358 -0
- package/proof/oracle/src/core/connection.ts +41 -0
- package/proof/oracle/src/core/host.ts +882 -0
- package/proof/oracle/src/core/sequencer.ts +75 -0
- package/proof/oracle/src/core/state-store.ts +162 -0
- package/proof/oracle/src/core/uri.ts +25 -0
- package/proof/oracle/src/host/direct-settings.ts +110 -0
- package/proof/oracle/src/host/pi-host.ts +220 -0
- package/proof/oracle/src/host/serve.ts +71 -0
- package/proof/oracle/src/host/terminal-service.ts +346 -0
- package/proof/oracle/src/pi/activity.ts +171 -0
- package/proof/oracle/src/pi/changeset-service.ts +646 -0
- package/proof/oracle/src/pi/changeset-uri.ts +64 -0
- package/proof/oracle/src/pi/chat-driver.ts +529 -0
- package/proof/oracle/src/pi/completions.ts +160 -0
- package/proof/oracle/src/pi/delete-session.ts +53 -0
- package/proof/oracle/src/pi/event-mapper.ts +648 -0
- package/proof/oracle/src/pi/git-changes.ts +605 -0
- package/proof/oracle/src/pi/history.ts +305 -0
- package/proof/oracle/src/pi/image-input.ts +56 -0
- package/proof/oracle/src/pi/image-mime.ts +8 -0
- package/proof/oracle/src/pi/in-process-backend.ts +155 -0
- package/proof/oracle/src/pi/message-input.ts +212 -0
- package/proof/oracle/src/pi/models.ts +124 -0
- package/proof/oracle/src/pi/project-trust.ts +66 -0
- package/proof/oracle/src/pi/provider.ts +2 -0
- package/proof/oracle/src/pi/resource-paths.ts +80 -0
- package/proof/oracle/src/pi/resource-service.ts +356 -0
- package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
- package/proof/oracle/src/pi/resource-watch.ts +357 -0
- package/proof/oracle/src/pi/session-catalogue.ts +318 -0
- package/proof/oracle/src/pi/session-config.ts +84 -0
- package/proof/oracle/src/pi/session-history.ts +75 -0
- package/proof/oracle/src/pi/session-hydrator.ts +219 -0
- package/proof/oracle/src/pi/session-registry.ts +815 -0
- package/proof/oracle/src/pi/session-storage.ts +29 -0
- package/proof/oracle/src/pi/session-title.ts +18 -0
- package/proof/oracle/src/pi/turn-paging.ts +94 -0
- package/proof/oracle/src/pi/user-message.ts +66 -0
- package/proof/oracle/src/protocol/errors.ts +44 -0
- package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
- package/proof/oracle/src/protocol/version.ts +39 -0
- package/proof/oracle/src/transport/websocket.ts +136 -0
- package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
- package/proof/oracle/src/tunnel/discovery.ts +37 -0
- package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
- package/proof/oracle/test/activity.test.ts +171 -0
- package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
- package/proof/oracle/test/changeset-uri.test.ts +37 -0
- package/proof/oracle/test/changeset.test.serial.ts +487 -0
- package/proof/oracle/test/chat-driver.test.ts +691 -0
- package/proof/oracle/test/client-actions.test.ts +409 -0
- package/proof/oracle/test/client-workarounds.test.ts +320 -0
- package/proof/oracle/test/completions.test.ts +323 -0
- package/proof/oracle/test/delete-session.test.ts +142 -0
- package/proof/oracle/test/direct-settings.test.ts +81 -0
- package/proof/oracle/test/event-mapper.test.ts +619 -0
- package/proof/oracle/test/fetch-turns.test.ts +207 -0
- package/proof/oracle/test/fixtures/abort.json +711 -0
- package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
- package/proof/oracle/test/fixtures/compaction.json +6285 -0
- package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
- package/proof/oracle/test/fixtures/plain-text.json +1856 -0
- package/proof/oracle/test/fixtures/single-tool.json +1128 -0
- package/proof/oracle/test/fixtures/steering.json +3926 -0
- package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
- package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
- package/proof/oracle/test/fixtures/tool-error.json +2850 -0
- package/proof/oracle/test/fixtures/tool-find.json +1162 -0
- package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
- package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
- package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
- package/proof/oracle/test/fixtures/tool-write.json +1271 -0
- package/proof/oracle/test/handshake.test.ts +275 -0
- package/proof/oracle/test/harness.ts +153 -0
- package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
- package/proof/oracle/test/image-input.test.ts +28 -0
- package/proof/oracle/test/image-session.test.ts +69 -0
- package/proof/oracle/test/live-turn.test.ts +304 -0
- package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
- package/proof/oracle/test/message-input.test.ts +181 -0
- package/proof/oracle/test/model-discovery.test.ts +170 -0
- package/proof/oracle/test/models.test.ts +109 -0
- package/proof/oracle/test/pi-host.test.ts +91 -0
- package/proof/oracle/test/pi-replay.test.ts +141 -0
- package/proof/oracle/test/project-trust.test.ts +125 -0
- package/proof/oracle/test/protocol-surface.test.ts +121 -0
- package/proof/oracle/test/pty.test.ts +100 -0
- package/proof/oracle/test/reconnect.test.ts +389 -0
- package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
- package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
- package/proof/oracle/test/resource.test.ts +499 -0
- package/proof/oracle/test/schema.test.ts +88 -0
- package/proof/oracle/test/session-catalogue.test.ts +314 -0
- package/proof/oracle/test/session-config.test.ts +224 -0
- package/proof/oracle/test/session-disposal.test.ts +364 -0
- package/proof/oracle/test/session-hydration.test.ts +194 -0
- package/proof/oracle/test/session-lifecycle.test.ts +416 -0
- package/proof/oracle/test/session-storage.test.serial.ts +32 -0
- package/proof/oracle/test/session-summary.test.ts +418 -0
- package/proof/oracle/test/subscriptions.test.ts +198 -0
- package/proof/oracle/test/support/assertions.ts +32 -0
- package/proof/oracle/test/support/async.ts +26 -0
- package/proof/oracle/test/support/hydrated-session.ts +205 -0
- package/proof/oracle/test/support/images.ts +8 -0
- package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
- package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
- package/proof/oracle/test/support/replay.ts +194 -0
- package/proof/oracle/test/support/schema.ts +221 -0
- package/proof/oracle/test/support/session-files.ts +12 -0
- package/proof/oracle/test/support/session-storage.ts +22 -0
- package/proof/oracle/test/support/upstream.ts +25 -0
- package/proof/oracle/test/support/watch-events.ts +108 -0
- package/proof/oracle/test/terminal-service.test.ts +495 -0
- package/proof/oracle/test/truncate.test.ts +230 -0
- package/proof/oracle/test/tunnel.test.ts +258 -0
- package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
- package/proof/oracle/test/uri.test.ts +24 -0
- package/proof/oracle/test/watch-events.test.ts +118 -0
- package/proof/oracle/tsconfig.json +32 -0
- package/proof/proof-piglet/piglet.yaml +18 -0
- package/proof/tools/fakellm/go.mod +3 -0
- package/proof/tools/fakellm/main.go +109 -0
- package/proof/tools/gen-dispatchable.py +30 -0
- package/provenance.json +28 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
package pi_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/base64"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"sync"
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
"github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
11
|
+
|
|
12
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/host"
|
|
13
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
|
|
14
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/pi"
|
|
15
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/testkit"
|
|
16
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/wire"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// onePixelPNG is upstream's test/support/images.ts ONE_PIXEL_PNG.
|
|
20
|
+
const onePixelPNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
|
21
|
+
|
|
22
|
+
type obj = map[string]any
|
|
23
|
+
|
|
24
|
+
// event decodes a JSON literal into a Pi event.
|
|
25
|
+
func event(t testing.TB, literal string) mapper.Event {
|
|
26
|
+
t.Helper()
|
|
27
|
+
var e mapper.Event
|
|
28
|
+
if err := json.Unmarshal([]byte(literal), &e); err != nil {
|
|
29
|
+
t.Fatal(err)
|
|
30
|
+
}
|
|
31
|
+
return e
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// say is upstream's chat-driver test script: one assistant message that says text, then settles.
|
|
35
|
+
func say(t testing.TB, text string) []mapper.Event {
|
|
36
|
+
quoted, _ := json.Marshal(text)
|
|
37
|
+
return []mapper.Event{
|
|
38
|
+
event(t, `{"type":"agent_start"}`),
|
|
39
|
+
event(t, `{"type":"message_start","message":{"role":"assistant"}}`),
|
|
40
|
+
event(t, `{"type":"message_update","message":{"role":"assistant"},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":`+string(quoted)+`}}`),
|
|
41
|
+
event(t, `{"type":"message_end","message":{"role":"assistant"}}`),
|
|
42
|
+
event(t, `{"type":"agent_end","messages":[],"willRetry":false}`),
|
|
43
|
+
event(t, `{"type":"agent_settled"}`),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// release is a gate a test can open exactly once.
|
|
48
|
+
type release struct {
|
|
49
|
+
once sync.Once
|
|
50
|
+
ch chan struct{}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func newRelease() *release { return &release{ch: make(chan struct{})} }
|
|
54
|
+
func (r *release) open() { r.once.Do(func() { close(r.ch) }) }
|
|
55
|
+
|
|
56
|
+
// scriptedBackend records prompts and replays a scripted event sequence for each one, so a
|
|
57
|
+
// turn's shape is fully determined by the test (upstream ScriptedBackend).
|
|
58
|
+
type scriptedBackend struct {
|
|
59
|
+
mu sync.Mutex
|
|
60
|
+
prompts []string
|
|
61
|
+
promptImages [][]mapper.Image
|
|
62
|
+
steers []string
|
|
63
|
+
steerImages [][]mapper.Image
|
|
64
|
+
promptCalls int
|
|
65
|
+
aborts int
|
|
66
|
+
selections int
|
|
67
|
+
promptGate *release
|
|
68
|
+
selectionGate *release
|
|
69
|
+
abortGate *release
|
|
70
|
+
listeners map[int]func(mapper.Event)
|
|
71
|
+
nextListener int
|
|
72
|
+
script func(text string) []mapper.Event
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func newScriptedBackend(script func(string) []mapper.Event) *scriptedBackend {
|
|
76
|
+
return &scriptedBackend{script: script, listeners: map[int]func(mapper.Event){}}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func (b *scriptedBackend) Subscribe(l func(mapper.Event)) func() {
|
|
80
|
+
b.mu.Lock()
|
|
81
|
+
defer b.mu.Unlock()
|
|
82
|
+
id := b.nextListener
|
|
83
|
+
b.nextListener++
|
|
84
|
+
b.listeners[id] = l
|
|
85
|
+
return func() { b.mu.Lock(); delete(b.listeners, id); b.mu.Unlock() }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func (b *scriptedBackend) emit(e mapper.Event) {
|
|
89
|
+
b.mu.Lock()
|
|
90
|
+
ls := make([]func(mapper.Event), 0, len(b.listeners))
|
|
91
|
+
for _, l := range b.listeners {
|
|
92
|
+
ls = append(ls, l)
|
|
93
|
+
}
|
|
94
|
+
b.mu.Unlock()
|
|
95
|
+
for _, l := range ls {
|
|
96
|
+
l(e)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func (b *scriptedBackend) Prompt(ctx context.Context, text string, images []mapper.Image) error {
|
|
101
|
+
b.mu.Lock()
|
|
102
|
+
b.promptCalls++
|
|
103
|
+
gate := b.promptGate
|
|
104
|
+
b.mu.Unlock()
|
|
105
|
+
if gate != nil {
|
|
106
|
+
<-gate.ch
|
|
107
|
+
}
|
|
108
|
+
if ctx.Err() != nil {
|
|
109
|
+
return nil
|
|
110
|
+
}
|
|
111
|
+
b.mu.Lock()
|
|
112
|
+
b.prompts = append(b.prompts, text)
|
|
113
|
+
b.promptImages = append(b.promptImages, images)
|
|
114
|
+
b.mu.Unlock()
|
|
115
|
+
// Deliver like a real agent: the driver must not depend on events arriving inside Prompt.
|
|
116
|
+
for _, e := range b.script(text) {
|
|
117
|
+
b.emit(e)
|
|
118
|
+
}
|
|
119
|
+
return nil
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func (b *scriptedBackend) Steer(_ context.Context, text string, images []mapper.Image) error {
|
|
123
|
+
b.mu.Lock()
|
|
124
|
+
defer b.mu.Unlock()
|
|
125
|
+
b.steers = append(b.steers, text)
|
|
126
|
+
b.steerImages = append(b.steerImages, images)
|
|
127
|
+
return nil
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func (b *scriptedBackend) Abort(context.Context) error {
|
|
131
|
+
b.mu.Lock()
|
|
132
|
+
b.aborts++
|
|
133
|
+
gate := b.abortGate
|
|
134
|
+
b.mu.Unlock()
|
|
135
|
+
if gate != nil {
|
|
136
|
+
<-gate.ch
|
|
137
|
+
}
|
|
138
|
+
return nil
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func (b *scriptedBackend) SelectModel(context.Context, ahptypes.ModelSelection) error {
|
|
142
|
+
b.mu.Lock()
|
|
143
|
+
b.selections++
|
|
144
|
+
gate := b.selectionGate
|
|
145
|
+
b.mu.Unlock()
|
|
146
|
+
if gate != nil {
|
|
147
|
+
<-gate.ch
|
|
148
|
+
}
|
|
149
|
+
return nil
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func (b *scriptedBackend) count(field func(*scriptedBackend) int) int {
|
|
153
|
+
b.mu.Lock()
|
|
154
|
+
defer b.mu.Unlock()
|
|
155
|
+
return field(b)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func (b *scriptedBackend) promptList() []string {
|
|
159
|
+
b.mu.Lock()
|
|
160
|
+
defer b.mu.Unlock()
|
|
161
|
+
return append([]string(nil), b.prompts...)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// funcBackend is a backend made of closures.
|
|
165
|
+
type funcBackend struct {
|
|
166
|
+
subscribe func(func(mapper.Event)) func()
|
|
167
|
+
prompt func(ctx context.Context, text string) error
|
|
168
|
+
steer func(ctx context.Context, text string) error
|
|
169
|
+
abort func(ctx context.Context) error
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func (b funcBackend) Subscribe(l func(mapper.Event)) func() {
|
|
173
|
+
if b.subscribe == nil {
|
|
174
|
+
return func() {}
|
|
175
|
+
}
|
|
176
|
+
return b.subscribe(l)
|
|
177
|
+
}
|
|
178
|
+
func (b funcBackend) Prompt(ctx context.Context, text string, _ []mapper.Image) error {
|
|
179
|
+
if b.prompt == nil {
|
|
180
|
+
return nil
|
|
181
|
+
}
|
|
182
|
+
return b.prompt(ctx, text)
|
|
183
|
+
}
|
|
184
|
+
func (b funcBackend) Steer(ctx context.Context, text string, _ []mapper.Image) error {
|
|
185
|
+
if b.steer == nil {
|
|
186
|
+
return nil
|
|
187
|
+
}
|
|
188
|
+
return b.steer(ctx, text)
|
|
189
|
+
}
|
|
190
|
+
func (b funcBackend) Abort(ctx context.Context) error {
|
|
191
|
+
if b.abort == nil {
|
|
192
|
+
return nil
|
|
193
|
+
}
|
|
194
|
+
return b.abort(ctx)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// fixture is a host with a session registry over in-memory storage and one connected client.
|
|
198
|
+
type fixture struct {
|
|
199
|
+
t *testing.T
|
|
200
|
+
host *host.Host
|
|
201
|
+
sessions *pi.Registry
|
|
202
|
+
client *testkit.Client
|
|
203
|
+
sessionChannel string
|
|
204
|
+
chatChannel string
|
|
205
|
+
id string
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
var uuidCounter struct {
|
|
209
|
+
sync.Mutex
|
|
210
|
+
n int
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func newID() string {
|
|
214
|
+
uuidCounter.Lock()
|
|
215
|
+
defer uuidCounter.Unlock()
|
|
216
|
+
uuidCounter.n++
|
|
217
|
+
return "0190a1b2-c3d4-7e5f-8a9b-" + pad(uuidCounter.n)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func pad(n int) string {
|
|
221
|
+
s := "000000000000"
|
|
222
|
+
d := []byte(s)
|
|
223
|
+
for i := len(d) - 1; i >= 0 && n > 0; i-- {
|
|
224
|
+
d[i] = byte('0' + n%10)
|
|
225
|
+
n /= 10
|
|
226
|
+
}
|
|
227
|
+
return string(d)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
func (f *fixture) chat() *ahptypes.ChatState { return f.host.Store().Chat(f.chatChannel) }
|
|
231
|
+
func (f *fixture) session() *ahptypes.SessionState { return f.host.Store().Session(f.sessionChannel) }
|
|
232
|
+
|
|
233
|
+
// start creates the host and registry (no session yet).
|
|
234
|
+
func startWith(t *testing.T, opts pi.RegistryOptions) (*host.Host, *pi.Registry) {
|
|
235
|
+
t.Helper()
|
|
236
|
+
h := testkit.NewHost(host.Options{})
|
|
237
|
+
opts.Host = h
|
|
238
|
+
if opts.CreateSessionManager == nil {
|
|
239
|
+
opts.CreateSessionManager = pi.InMemoryStorage
|
|
240
|
+
}
|
|
241
|
+
reg := pi.NewRegistry(opts)
|
|
242
|
+
h.Serve(host.Capabilities{Sessions: reg, TurnPaging: fetchTurns{reg}})
|
|
243
|
+
return h, reg
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
type fetchTurns struct{ r *pi.Registry }
|
|
247
|
+
|
|
248
|
+
func (f fetchTurns) FetchTurns(ctx context.Context, p ahptypes.FetchTurnsParams) error {
|
|
249
|
+
return f.r.FetchTurns(ctx, p)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// startFixture is upstream's chat-driver startFixture: a created session with both channels
|
|
253
|
+
// subscribed.
|
|
254
|
+
func startFixture(t *testing.T, backend pi.Backend) *fixture {
|
|
255
|
+
t.Helper()
|
|
256
|
+
h, reg := startWith(t, pi.RegistryOptions{CreateBackend: func(*pi.LiveSession) (pi.Backend, error) { return backend, nil }})
|
|
257
|
+
client := testkit.Connect(t, h)
|
|
258
|
+
t.Cleanup(client.Close)
|
|
259
|
+
client.Initialize("driver-client", nil)
|
|
260
|
+
id := newID()
|
|
261
|
+
f := &fixture{t: t, host: h, sessions: reg, client: client, id: id, sessionChannel: wire.SessionURI(id), chatChannel: wire.ChatURI(id)}
|
|
262
|
+
client.Must("createSession", obj{"channel": f.sessionChannel})
|
|
263
|
+
client.Subscribe(f.sessionChannel)
|
|
264
|
+
client.Subscribe(f.chatChannel)
|
|
265
|
+
// The session is ready once its backend is attached.
|
|
266
|
+
testkit.Eventually(t, "the session to become ready", func() bool {
|
|
267
|
+
s := f.session()
|
|
268
|
+
return s != nil && s.Lifecycle == ahptypes.SessionLifecycleReady
|
|
269
|
+
})
|
|
270
|
+
return f
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
func userMessage(text string) obj {
|
|
274
|
+
return obj{"text": text, "origin": obj{"kind": "user"}}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
func embeddedText(text, label string) obj {
|
|
278
|
+
return obj{"type": "embeddedResource", "label": label, "contentType": "text/plain", "data": base64.StdEncoding.EncodeToString([]byte(text))}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
func embeddedImage(label string) obj {
|
|
282
|
+
return obj{"type": "embeddedResource", "label": label, "displayKind": "image", "contentType": "image/png", "data": onePixelPNG}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
func (f *fixture) turnStarted(turnID string, message obj) {
|
|
286
|
+
f.client.Dispatch(f.chatChannel, obj{"type": "chat/turnStarted", "turnId": turnID, "startedAt": "2025-01-01T00:00:00.000Z", "message": message})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
func (f *fixture) pending(kind, id string, message obj) {
|
|
290
|
+
f.client.Dispatch(f.chatChannel, obj{"type": "chat/pendingMessageSet", "kind": kind, "id": id, "message": message})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
func (f *fixture) cancel(turnID string) {
|
|
294
|
+
f.client.Dispatch(f.chatChannel, obj{"type": "chat/turnCancelled", "turnId": turnID, "duration": 0})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
type turnShape struct {
|
|
298
|
+
ID, Text string
|
|
299
|
+
State ahptypes.TurnState
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
func turnShapes(turns []ahptypes.Turn) []turnShape {
|
|
303
|
+
out := []turnShape{}
|
|
304
|
+
for _, t := range turns {
|
|
305
|
+
out = append(out, turnShape{t.Id, t.Message.Text, t.State})
|
|
306
|
+
}
|
|
307
|
+
return out
|
|
308
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
package pi_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
|
|
6
|
+
ahptypes "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
7
|
+
|
|
8
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/host"
|
|
9
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
|
|
10
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/pi"
|
|
11
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/testkit"
|
|
12
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/wire"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// Additions (not upstream twins): a turn nobody dispatched through AHP, typed into the running
|
|
16
|
+
// PiG session, is mirrored into the chat when AdoptForeignTurns is on; upstream's host owns its
|
|
17
|
+
// agent, so it never sees one and ignores such events (which stays the default).
|
|
18
|
+
|
|
19
|
+
func foreignFixture(t *testing.T, adopt bool) (*fixture, *scriptedBackend) {
|
|
20
|
+
t.Helper()
|
|
21
|
+
backend := newScriptedBackend(func(string) []mapper.Event { return nil })
|
|
22
|
+
h, reg := startWith(t, pi.RegistryOptions{
|
|
23
|
+
CreateBackend: func(*pi.LiveSession) (pi.Backend, error) { return backend, nil },
|
|
24
|
+
AdoptForeignTurns: adopt,
|
|
25
|
+
})
|
|
26
|
+
client := testkit.Connect(t, h)
|
|
27
|
+
t.Cleanup(client.Close)
|
|
28
|
+
client.Initialize("foreign-client", nil)
|
|
29
|
+
id := newID()
|
|
30
|
+
f := &fixture{t: t, host: h, sessions: reg, client: client, id: id, sessionChannel: wire.SessionURI(id), chatChannel: wire.ChatURI(id)}
|
|
31
|
+
client.Must("createSession", obj{"channel": f.sessionChannel})
|
|
32
|
+
client.Subscribe(f.sessionChannel)
|
|
33
|
+
client.Subscribe(f.chatChannel)
|
|
34
|
+
testkit.Eventually(t, "the session to become ready", func() bool {
|
|
35
|
+
s := f.session()
|
|
36
|
+
return s != nil && s.Lifecycle == ahptypes.SessionLifecycleReady
|
|
37
|
+
})
|
|
38
|
+
return f, backend
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func localPrompt(t *testing.T, backend *scriptedBackend, text, answer string) {
|
|
42
|
+
backend.emit(event(t, `{"type":"agent_start"}`))
|
|
43
|
+
backend.emit(event(t, `{"type":"turn_start"}`))
|
|
44
|
+
backend.emit(event(t, `{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"`+text+`"}]}}`))
|
|
45
|
+
backend.emit(event(t, `{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"`+text+`"}]}}`))
|
|
46
|
+
for _, e := range say(t, answer)[1:] {
|
|
47
|
+
backend.emit(e)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func TestForeignTurnsAreMirrored(t *testing.T) {
|
|
52
|
+
f, backend := foreignFixture(t, true)
|
|
53
|
+
localPrompt(t, backend, "typed locally", "answered locally")
|
|
54
|
+
testkit.Eventually(t, "the local turn to complete", func() bool {
|
|
55
|
+
c := f.chat()
|
|
56
|
+
return c != nil && len(c.Turns) == 1 && len(c.Turns[0].ResponseParts) > 0 && c.Activity == nil
|
|
57
|
+
})
|
|
58
|
+
turn := f.chat().Turns[0]
|
|
59
|
+
if turn.Message.Text != "typed locally" {
|
|
60
|
+
t.Fatalf("user message %q", turn.Message.Text)
|
|
61
|
+
}
|
|
62
|
+
if len(turn.ResponseParts) == 0 {
|
|
63
|
+
t.Fatal("the local answer never reached the chat")
|
|
64
|
+
}
|
|
65
|
+
// a second local prompt is a second turn
|
|
66
|
+
localPrompt(t, backend, "again", "second")
|
|
67
|
+
testkit.Eventually(t, "the second local turn", func() bool { return len(f.chat().Turns) == 2 })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func TestForeignTurnsAreIgnoredByDefault(t *testing.T) {
|
|
71
|
+
f, backend := foreignFixture(t, false)
|
|
72
|
+
localPrompt(t, backend, "typed locally", "answered locally")
|
|
73
|
+
if !f.client.Silent(func(n testkit.Notification) bool { env, ok := n.Envelope(); return ok && env.Channel == f.chatChannel }, 200_000_000) {
|
|
74
|
+
t.Fatal("a turn nobody started must be ignored unless AdoptForeignTurns is set")
|
|
75
|
+
}
|
|
76
|
+
if n := len(f.chat().Turns); n != 0 {
|
|
77
|
+
t.Fatalf("%d turns", n)
|
|
78
|
+
}
|
|
79
|
+
_ = host.Options{}
|
|
80
|
+
_ = mapper.Event{}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func TestCancelledTailIsNotAdopted(t *testing.T) {
|
|
84
|
+
f, backend := foreignFixture(t, true)
|
|
85
|
+
f.turnStarted("remote-1", userMessage("go"))
|
|
86
|
+
testkit.Eventually(t, "the prompt to reach the backend", func() bool { return backend.count(func(b *scriptedBackend) int { return b.promptCalls }) == 1 })
|
|
87
|
+
f.cancel("remote-1")
|
|
88
|
+
testkit.Eventually(t, "the cancel to reach the backend", func() bool { return backend.count(func(b *scriptedBackend) int { return b.aborts }) >= 1 })
|
|
89
|
+
// the aborted run's trailing events carry no user message, so they start nothing
|
|
90
|
+
for _, e := range say(t, "late")[1:] {
|
|
91
|
+
backend.emit(e)
|
|
92
|
+
}
|
|
93
|
+
if n := len(f.chat().Turns); n != 1 {
|
|
94
|
+
t.Fatalf("%d turns after a cancelled run's tail", n)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// While a cancellation is settling, a user message must not open a turn: it can only be the cancelled
|
|
99
|
+
// run's own input replaying, and adopting it would resurrect the turn the client just cancelled.
|
|
100
|
+
func TestNothingIsAdoptedWhileACancellationSettles(t *testing.T) {
|
|
101
|
+
f, backend := foreignFixture(t, true)
|
|
102
|
+
gate := newRelease()
|
|
103
|
+
backend.mu.Lock()
|
|
104
|
+
backend.abortGate = gate
|
|
105
|
+
backend.mu.Unlock()
|
|
106
|
+
f.turnStarted("remote-1", userMessage("go"))
|
|
107
|
+
testkit.Eventually(t, "the prompt to reach the backend", func() bool { return backend.count(func(b *scriptedBackend) int { return b.promptCalls }) == 1 })
|
|
108
|
+
f.cancel("remote-1")
|
|
109
|
+
testkit.Eventually(t, "the abort to be in flight", func() bool { return backend.count(func(b *scriptedBackend) int { return b.aborts }) == 1 })
|
|
110
|
+
|
|
111
|
+
backend.emit(event(t, `{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"replayed"}]}}`))
|
|
112
|
+
if n, active := len(f.chat().Turns), f.chat().ActiveTurn; n != 1 || active != nil {
|
|
113
|
+
t.Fatalf("a message during the settling cancellation opened a turn: %d turns, active=%v", n, active != nil)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
gate.open()
|
|
117
|
+
// The settling has no signal of its own: a user message is offered until one is adopted.
|
|
118
|
+
testkit.Eventually(t, "a local prompt after the cancellation to be mirrored", func() bool {
|
|
119
|
+
backend.emit(event(t, `{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"typed later"}]}}`))
|
|
120
|
+
return f.chat().ActiveTurn != nil
|
|
121
|
+
})
|
|
122
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package pi_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"sync"
|
|
6
|
+
"sync/atomic"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/host"
|
|
10
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/pi"
|
|
11
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/testkit"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// harness is upstream test/harness.ts: a live host with the session services over a fixture-owned
|
|
15
|
+
// session root. Clients attach in memory (the transport itself is covered by internal/ws).
|
|
16
|
+
type harness struct {
|
|
17
|
+
t *testing.T
|
|
18
|
+
host *host.Host
|
|
19
|
+
services *pi.Services
|
|
20
|
+
root string
|
|
21
|
+
|
|
22
|
+
mu sync.Mutex
|
|
23
|
+
deletedFiles []string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type harnessOptions struct {
|
|
27
|
+
createBackend pi.BackendFactory
|
|
28
|
+
deleteFile func(path string) (pi.SessionFileDeletionResult, error)
|
|
29
|
+
sessionRoot string
|
|
30
|
+
replayBuffer int
|
|
31
|
+
workingDir string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func startHarness(t *testing.T, o harnessOptions) *harness {
|
|
35
|
+
t.Helper()
|
|
36
|
+
h := &harness{t: t}
|
|
37
|
+
h.root = o.sessionRoot
|
|
38
|
+
if h.root == "" {
|
|
39
|
+
h.root = t.TempDir()
|
|
40
|
+
}
|
|
41
|
+
h.host = testkit.NewHost(host.Options{ReplayBufferCapacity: o.replayBuffer})
|
|
42
|
+
deleteFile := o.deleteFile
|
|
43
|
+
if deleteFile == nil {
|
|
44
|
+
deleteFile = func(path string) (pi.SessionFileDeletionResult, error) {
|
|
45
|
+
h.mu.Lock()
|
|
46
|
+
h.deletedFiles = append(h.deletedFiles, path)
|
|
47
|
+
h.mu.Unlock()
|
|
48
|
+
return pi.SessionFileDeletionResult{OK: true}, nil
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
workingDir := o.workingDir
|
|
52
|
+
if workingDir == "" {
|
|
53
|
+
workingDir = t.TempDir()
|
|
54
|
+
}
|
|
55
|
+
h.services = pi.NewServices(pi.ServicesOptions{
|
|
56
|
+
Host: h.host, SessionRoot: h.root, DefaultWorkingDirectory: workingDir,
|
|
57
|
+
CreateBackend: o.createBackend, CreateSessionManager: pi.PersistentStorage(filepath.Join(h.root, "created")),
|
|
58
|
+
DeleteFile: deleteFile,
|
|
59
|
+
})
|
|
60
|
+
h.host.Serve(h.services.Capabilities())
|
|
61
|
+
return h
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func (h *harness) deleted() []string {
|
|
65
|
+
h.mu.Lock()
|
|
66
|
+
defer h.mu.Unlock()
|
|
67
|
+
return append([]string(nil), h.deletedFiles...)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var clientCounter atomic.Int64
|
|
71
|
+
|
|
72
|
+
func nextClientID() string { return "test-client-" + itoa(int(clientCounter.Add(1))) }
|
|
73
|
+
|
|
74
|
+
func itoa(i int) string {
|
|
75
|
+
if i == 0 {
|
|
76
|
+
return "0"
|
|
77
|
+
}
|
|
78
|
+
var d []byte
|
|
79
|
+
for ; i > 0; i /= 10 {
|
|
80
|
+
d = append([]byte{byte('0' + i%10)}, d...)
|
|
81
|
+
}
|
|
82
|
+
return string(d)
|
|
83
|
+
}
|