@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,452 @@
|
|
|
1
|
+
package svc
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"errors"
|
|
6
|
+
"fmt"
|
|
7
|
+
"sync"
|
|
8
|
+
"testing"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/testkit"
|
|
12
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/twin"
|
|
13
|
+
ahptypes "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// watchEvents is the test-side journal of one watch subscription: one reader per subscription, a
|
|
17
|
+
// retained event journal, assertions that observe it (port of test/support/watch-events.ts).
|
|
18
|
+
|
|
19
|
+
type batchSource interface {
|
|
20
|
+
// Next blocks for the next batch; it returns errStopped once stop closes.
|
|
21
|
+
Next(stop <-chan struct{}) ([]ahptypes.ResourceChange, error)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var errStopped = errors.New("watch reader stopped")
|
|
25
|
+
|
|
26
|
+
type watchEvents struct {
|
|
27
|
+
src batchSource
|
|
28
|
+
clock Clock
|
|
29
|
+
describe func() any
|
|
30
|
+
|
|
31
|
+
mu sync.Mutex
|
|
32
|
+
batches [][]ahptypes.ResourceChange
|
|
33
|
+
err error
|
|
34
|
+
closed bool
|
|
35
|
+
changed chan struct{}
|
|
36
|
+
|
|
37
|
+
stop chan struct{}
|
|
38
|
+
reading chan struct{}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func newWatchEvents(src batchSource, clock Clock, describe func() any) *watchEvents {
|
|
42
|
+
if clock == nil {
|
|
43
|
+
clock = realClock{}
|
|
44
|
+
}
|
|
45
|
+
if describe == nil {
|
|
46
|
+
describe = func() any { return map[string]any{} }
|
|
47
|
+
}
|
|
48
|
+
e := &watchEvents{src: src, clock: clock, describe: describe, changed: make(chan struct{}), stop: make(chan struct{}), reading: make(chan struct{})}
|
|
49
|
+
go e.read()
|
|
50
|
+
return e
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func (e *watchEvents) notifyLocked() {
|
|
54
|
+
close(e.changed)
|
|
55
|
+
e.changed = make(chan struct{})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func (e *watchEvents) read() {
|
|
59
|
+
defer close(e.reading)
|
|
60
|
+
for {
|
|
61
|
+
batch, err := e.src.Next(e.stop)
|
|
62
|
+
e.mu.Lock()
|
|
63
|
+
if e.closed {
|
|
64
|
+
e.mu.Unlock()
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
if err != nil {
|
|
68
|
+
e.err = err
|
|
69
|
+
e.notifyLocked()
|
|
70
|
+
e.mu.Unlock()
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
e.batches = append(e.batches, batch)
|
|
74
|
+
e.notifyLocked()
|
|
75
|
+
e.mu.Unlock()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func (e *watchEvents) changesLocked() []ahptypes.ResourceChange {
|
|
80
|
+
var all []ahptypes.ResourceChange
|
|
81
|
+
for _, b := range e.batches {
|
|
82
|
+
all = append(all, b...)
|
|
83
|
+
}
|
|
84
|
+
return all
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func (e *watchEvents) Changes() []ahptypes.ResourceChange {
|
|
88
|
+
e.mu.Lock()
|
|
89
|
+
defer e.mu.Unlock()
|
|
90
|
+
return e.changesLocked()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func (e *watchEvents) Batches() [][]ahptypes.ResourceChange {
|
|
94
|
+
e.mu.Lock()
|
|
95
|
+
defer e.mu.Unlock()
|
|
96
|
+
return append([][]ahptypes.ResourceChange(nil), e.batches...)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func (e *watchEvents) Mark() int { return len(e.Changes()) }
|
|
100
|
+
|
|
101
|
+
// WaitFor blocks until complete accepts the changes since a checkpoint. A timeout stops only this
|
|
102
|
+
// assertion; it never starts or strands another read.
|
|
103
|
+
func (e *watchEvents) WaitFor(label string, complete func([]ahptypes.ResourceChange) bool, since int, timeout time.Duration) ([]ahptypes.ResourceChange, error) {
|
|
104
|
+
timedOut := make(chan struct{})
|
|
105
|
+
timer := e.clock.AfterFunc(timeout, func() { close(timedOut) })
|
|
106
|
+
defer timer.Stop()
|
|
107
|
+
fail := func(cause error) error {
|
|
108
|
+
e.mu.Lock()
|
|
109
|
+
batches, _ := json.Marshal(e.batches)
|
|
110
|
+
e.mu.Unlock()
|
|
111
|
+
state, _ := json.Marshal(e.describe())
|
|
112
|
+
return fmt.Errorf("Expected %s; watch=%s; batches=%s: %w", label, state, batches, cause)
|
|
113
|
+
}
|
|
114
|
+
for {
|
|
115
|
+
e.mu.Lock()
|
|
116
|
+
if e.err != nil || e.closed {
|
|
117
|
+
err := e.err
|
|
118
|
+
if err == nil {
|
|
119
|
+
err = errors.New("Collector closed")
|
|
120
|
+
}
|
|
121
|
+
e.mu.Unlock()
|
|
122
|
+
return nil, fail(err)
|
|
123
|
+
}
|
|
124
|
+
changes := e.changesLocked()
|
|
125
|
+
if since > len(changes) {
|
|
126
|
+
since = len(changes)
|
|
127
|
+
}
|
|
128
|
+
changes = changes[since:]
|
|
129
|
+
if complete(changes) {
|
|
130
|
+
e.mu.Unlock()
|
|
131
|
+
return changes, nil
|
|
132
|
+
}
|
|
133
|
+
changed := e.changed
|
|
134
|
+
e.mu.Unlock()
|
|
135
|
+
select {
|
|
136
|
+
case <-changed:
|
|
137
|
+
case <-timedOut:
|
|
138
|
+
return nil, fail(errors.New("Timed out waiting for filesystem events"))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Close ends the reader, settles pending assertions and reports a reader failure.
|
|
144
|
+
func (e *watchEvents) Close() error {
|
|
145
|
+
e.mu.Lock()
|
|
146
|
+
if !e.closed {
|
|
147
|
+
e.closed = true
|
|
148
|
+
close(e.stop)
|
|
149
|
+
e.notifyLocked()
|
|
150
|
+
}
|
|
151
|
+
e.mu.Unlock()
|
|
152
|
+
<-e.reading
|
|
153
|
+
e.mu.Lock()
|
|
154
|
+
defer e.mu.Unlock()
|
|
155
|
+
return e.err
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// clientSource reads resourceWatch/changed batches for one channel from a testkit client.
|
|
159
|
+
type clientSource struct {
|
|
160
|
+
client *testkit.Client
|
|
161
|
+
channel string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func (s clientSource) Next(stop <-chan struct{}) ([]ahptypes.ResourceChange, error) {
|
|
165
|
+
for {
|
|
166
|
+
n, ok := s.client.Await(func(n testkit.Notification) bool {
|
|
167
|
+
if n.Method != "action" {
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
var p struct {
|
|
171
|
+
Channel string `json:"channel"`
|
|
172
|
+
Action struct {
|
|
173
|
+
Type string `json:"type"`
|
|
174
|
+
} `json:"action"`
|
|
175
|
+
}
|
|
176
|
+
return json.Unmarshal(n.Params, &p) == nil && p.Channel == s.channel && p.Action.Type == string(ahptypes.ActionTypeResourceWatchChanged)
|
|
177
|
+
}, 20*time.Millisecond)
|
|
178
|
+
if ok {
|
|
179
|
+
var p struct {
|
|
180
|
+
Action struct {
|
|
181
|
+
Changes struct {
|
|
182
|
+
Items []ahptypes.ResourceChange `json:"items"`
|
|
183
|
+
} `json:"changes"`
|
|
184
|
+
} `json:"action"`
|
|
185
|
+
}
|
|
186
|
+
if err := json.Unmarshal(n.Params, &p); err != nil {
|
|
187
|
+
return nil, err
|
|
188
|
+
}
|
|
189
|
+
return p.Action.Changes.Items, nil
|
|
190
|
+
}
|
|
191
|
+
select {
|
|
192
|
+
case <-stop:
|
|
193
|
+
return nil, errStopped
|
|
194
|
+
default:
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── a hand-driven clock ─────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
type fakeClock struct {
|
|
202
|
+
mu sync.Mutex
|
|
203
|
+
now time.Duration
|
|
204
|
+
timers []*fakeTimer
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
type fakeTimer struct {
|
|
208
|
+
c *fakeClock
|
|
209
|
+
at time.Duration
|
|
210
|
+
f func()
|
|
211
|
+
stopped bool
|
|
212
|
+
fired bool
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
func (c *fakeClock) AfterFunc(d time.Duration, f func()) Stopper {
|
|
216
|
+
c.mu.Lock()
|
|
217
|
+
defer c.mu.Unlock()
|
|
218
|
+
t := &fakeTimer{c: c, at: c.now + d, f: f}
|
|
219
|
+
c.timers = append(c.timers, t)
|
|
220
|
+
return t
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
func (t *fakeTimer) Stop() bool {
|
|
224
|
+
t.c.mu.Lock()
|
|
225
|
+
defer t.c.mu.Unlock()
|
|
226
|
+
was := !t.stopped && !t.fired
|
|
227
|
+
t.stopped = true
|
|
228
|
+
return was
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Tick advances the clock and runs every timer that came due, in order.
|
|
232
|
+
func (c *fakeClock) Tick(d time.Duration) {
|
|
233
|
+
c.mu.Lock()
|
|
234
|
+
target := c.now + d
|
|
235
|
+
c.mu.Unlock()
|
|
236
|
+
for {
|
|
237
|
+
c.mu.Lock()
|
|
238
|
+
var next *fakeTimer
|
|
239
|
+
for _, t := range c.timers {
|
|
240
|
+
if !t.stopped && !t.fired && t.at <= target && (next == nil || t.at < next.at) {
|
|
241
|
+
next = t
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if next == nil {
|
|
245
|
+
c.now = target
|
|
246
|
+
c.mu.Unlock()
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
next.fired = true
|
|
250
|
+
if next.at > c.now {
|
|
251
|
+
c.now = next.at
|
|
252
|
+
}
|
|
253
|
+
c.mu.Unlock()
|
|
254
|
+
next.f()
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── a scripted source ───────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
type scriptedSource struct {
|
|
261
|
+
mu sync.Mutex
|
|
262
|
+
reads int
|
|
263
|
+
pending chan scriptedResult
|
|
264
|
+
t *testing.T
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
type scriptedResult struct {
|
|
268
|
+
batch []ahptypes.ResourceChange
|
|
269
|
+
err error
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
func (s *scriptedSource) Next(stop <-chan struct{}) ([]ahptypes.ResourceChange, error) {
|
|
273
|
+
s.mu.Lock()
|
|
274
|
+
if s.pending != nil {
|
|
275
|
+
s.mu.Unlock()
|
|
276
|
+
s.t.Error("only one Next may be outstanding")
|
|
277
|
+
return nil, errStopped
|
|
278
|
+
}
|
|
279
|
+
s.reads++
|
|
280
|
+
ch := make(chan scriptedResult, 1)
|
|
281
|
+
s.pending = ch
|
|
282
|
+
s.mu.Unlock()
|
|
283
|
+
select {
|
|
284
|
+
case r := <-ch:
|
|
285
|
+
return r.batch, r.err
|
|
286
|
+
case <-stop:
|
|
287
|
+
s.mu.Lock()
|
|
288
|
+
if s.pending == ch {
|
|
289
|
+
s.pending = nil // detaching does not settle an outstanding read, like the SDK
|
|
290
|
+
}
|
|
291
|
+
s.mu.Unlock()
|
|
292
|
+
return nil, errStopped
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
func (s *scriptedSource) Reads() int { s.mu.Lock(); defer s.mu.Unlock(); return s.reads }
|
|
297
|
+
|
|
298
|
+
func (s *scriptedSource) resolve(r scriptedResult) {
|
|
299
|
+
// wait for the reader to park
|
|
300
|
+
for i := 0; i < 500; i++ {
|
|
301
|
+
s.mu.Lock()
|
|
302
|
+
ch := s.pending
|
|
303
|
+
s.pending = nil
|
|
304
|
+
s.mu.Unlock()
|
|
305
|
+
if ch != nil {
|
|
306
|
+
ch <- r
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
time.Sleep(2 * time.Millisecond)
|
|
310
|
+
}
|
|
311
|
+
s.t.Error("no read is outstanding")
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
func (s *scriptedSource) Push(items ...ahptypes.ResourceChange) {
|
|
315
|
+
s.resolve(scriptedResult{batch: items})
|
|
316
|
+
}
|
|
317
|
+
func (s *scriptedSource) Fail(err error) { s.resolve(scriptedResult{err: err}) }
|
|
318
|
+
|
|
319
|
+
func addedChange(name string) ahptypes.ResourceChange {
|
|
320
|
+
return ahptypes.ResourceChange{Uri: "file:///" + name, Type: ahptypes.ResourceChangeTypeAdded}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
func hasURI(uri string) func([]ahptypes.ResourceChange) bool {
|
|
324
|
+
return func(items []ahptypes.ResourceChange) bool {
|
|
325
|
+
for _, i := range items {
|
|
326
|
+
if i.Uri == uri {
|
|
327
|
+
return true
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return false
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Twins of upstream test/watch-events.test.ts: regression tests for the journal itself.
|
|
335
|
+
|
|
336
|
+
func TestWatchEventsJournal(t *testing.T) {
|
|
337
|
+
twin.Run(t, "watch-events", "watch assertions share a reader and retain the entire batch, even after a match", func(t *testing.T) {
|
|
338
|
+
input := &scriptedSource{t: t}
|
|
339
|
+
events := newWatchEvents(input, nil, nil)
|
|
340
|
+
defer events.Close()
|
|
341
|
+
type result struct {
|
|
342
|
+
changes []ahptypes.ResourceChange
|
|
343
|
+
err error
|
|
344
|
+
}
|
|
345
|
+
first, second := make(chan result, 1), make(chan result, 1)
|
|
346
|
+
go func() { c, err := events.WaitFor("a", hasURI("file:///a"), 0, 4*time.Second); first <- result{c, err} }()
|
|
347
|
+
go func() { c, err := events.WaitFor("b", hasURI("file:///b"), 0, 4*time.Second); second <- result{c, err} }()
|
|
348
|
+
testkit.Eventually(t, "the single reader parks", func() bool { return input.Reads() == 1 })
|
|
349
|
+
input.Push(addedChange("a"), addedChange("b"))
|
|
350
|
+
want := []ahptypes.ResourceChange{addedChange("a"), addedChange("b")}
|
|
351
|
+
for _, ch := range []chan result{first, second} {
|
|
352
|
+
r := <-ch
|
|
353
|
+
if r.err != nil || fmt.Sprint(r.changes) != fmt.Sprint(want) {
|
|
354
|
+
t.Fatalf("%+v", r)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if got := events.Batches(); len(got) != 1 || fmt.Sprint(got[0]) != fmt.Sprint(want) {
|
|
358
|
+
t.Fatalf("batches %v", got)
|
|
359
|
+
}
|
|
360
|
+
again, err := events.WaitFor("already received b", hasURI("file:///b"), 0, 4*time.Second)
|
|
361
|
+
if err != nil || fmt.Sprint(again) != fmt.Sprint(events.Changes()) {
|
|
362
|
+
t.Fatalf("%v %v", again, err)
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
twin.Run(t, "watch-events", "a timed-out assertion neither steals the next batch nor accepts events before its checkpoint", func(t *testing.T) {
|
|
367
|
+
clock := &fakeClock{}
|
|
368
|
+
input := &scriptedSource{t: t}
|
|
369
|
+
events := newWatchEvents(input, clock, func() any { return map[string]any{"exists": true} })
|
|
370
|
+
defer events.Close()
|
|
371
|
+
testkit.Eventually(t, "reader parks", func() bool { return input.Reads() == 1 })
|
|
372
|
+
input.Push(addedChange("a"))
|
|
373
|
+
if _, err := events.WaitFor("first a", func(items []ahptypes.ResourceChange) bool { return len(items) == 1 }, 0, time.Minute); err != nil {
|
|
374
|
+
t.Fatal(err)
|
|
375
|
+
}
|
|
376
|
+
since := events.Mark()
|
|
377
|
+
rejected := make(chan error, 1)
|
|
378
|
+
go func() {
|
|
379
|
+
_, err := events.WaitFor("second a", func(items []ahptypes.ResourceChange) bool { return len(items) > 0 }, since, 10*time.Millisecond)
|
|
380
|
+
rejected <- err
|
|
381
|
+
}()
|
|
382
|
+
// the assertion must have armed its timer before the clock moves
|
|
383
|
+
testkit.Eventually(t, "the assertion armed its timer", func() bool { clock.mu.Lock(); defer clock.mu.Unlock(); return len(clock.timers) >= 2 })
|
|
384
|
+
clock.Tick(10 * time.Millisecond)
|
|
385
|
+
err := <-rejected
|
|
386
|
+
if err == nil {
|
|
387
|
+
t.Fatal("the assertion must time out")
|
|
388
|
+
}
|
|
389
|
+
for _, want := range []string{"second a", `"exists":true`, "file:///a"} {
|
|
390
|
+
if !contains(err.Error(), want) {
|
|
391
|
+
t.Fatalf("error %q lacks %q", err, want)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if input.Reads() != 2 {
|
|
395
|
+
t.Fatalf("timeout must not start another reader, reads=%d", input.Reads())
|
|
396
|
+
}
|
|
397
|
+
next := make(chan []ahptypes.ResourceChange, 1)
|
|
398
|
+
go func() {
|
|
399
|
+
c, _ := events.WaitFor("second a", func(items []ahptypes.ResourceChange) bool { return len(items) > 0 }, since, time.Minute)
|
|
400
|
+
next <- c
|
|
401
|
+
}()
|
|
402
|
+
input.Push(addedChange("a"))
|
|
403
|
+
if got := <-next; fmt.Sprint(got) != fmt.Sprint([]ahptypes.ResourceChange{addedChange("a")}) {
|
|
404
|
+
t.Fatalf("%v", got)
|
|
405
|
+
}
|
|
406
|
+
if n := len(events.Batches()); n != 2 {
|
|
407
|
+
t.Fatalf("%d batches", n)
|
|
408
|
+
}
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
twin.Run(t, "watch-events", "reports a reader failure during cleanup even without a waiting assertion", func(t *testing.T) {
|
|
412
|
+
input := &scriptedSource{t: t}
|
|
413
|
+
events := newWatchEvents(input, nil, nil)
|
|
414
|
+
failure := errors.New("subscription read failed")
|
|
415
|
+
testkit.Eventually(t, "reader parks", func() bool { return input.Reads() == 1 })
|
|
416
|
+
input.Fail(failure)
|
|
417
|
+
time.Sleep(20 * time.Millisecond)
|
|
418
|
+
if err := events.Close(); err != failure {
|
|
419
|
+
t.Fatalf("got %v", err)
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
twin.Run(t, "watch-events", "closing the journal settles pending assertions and the sole subscription reader", func(t *testing.T) {
|
|
424
|
+
input := &scriptedSource{t: t}
|
|
425
|
+
events := newWatchEvents(input, nil, nil)
|
|
426
|
+
rejected := make(chan error, 1)
|
|
427
|
+
go func() {
|
|
428
|
+
_, err := events.WaitFor("never arrives", func(items []ahptypes.ResourceChange) bool { return len(items) > 0 }, 0, 4*time.Second)
|
|
429
|
+
rejected <- err
|
|
430
|
+
}()
|
|
431
|
+
testkit.Eventually(t, "reader parks", func() bool { return input.Reads() == 1 })
|
|
432
|
+
time.Sleep(10 * time.Millisecond)
|
|
433
|
+
if err := events.Close(); err != nil && !errors.Is(err, errStopped) {
|
|
434
|
+
t.Fatal(err)
|
|
435
|
+
}
|
|
436
|
+
if err := <-rejected; err == nil {
|
|
437
|
+
t.Fatal("a pending assertion must be rejected when the journal closes")
|
|
438
|
+
}
|
|
439
|
+
if input.Reads() != 1 {
|
|
440
|
+
t.Fatalf("reads=%d", input.Reads())
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
func contains(s, sub string) bool {
|
|
446
|
+
for i := 0; i+len(sub) <= len(s); i++ {
|
|
447
|
+
if s[i:i+len(sub)] == sub {
|
|
448
|
+
return true
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return false
|
|
452
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
package svc
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"strings"
|
|
6
|
+
|
|
7
|
+
ahptypes "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// RelativeWatchPath is path relative to root in slash form: "" for the root itself, and ok=false
|
|
11
|
+
// for a sibling, a parent or a prefix lookalike.
|
|
12
|
+
func RelativeWatchPath(root, path string) (string, bool) {
|
|
13
|
+
abs, err := filepath.Abs(path)
|
|
14
|
+
if err != nil {
|
|
15
|
+
return "", false
|
|
16
|
+
}
|
|
17
|
+
rel, err := filepath.Rel(root, abs)
|
|
18
|
+
if err != nil || filepath.IsAbs(rel) || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
19
|
+
return "", false
|
|
20
|
+
}
|
|
21
|
+
if rel == "." {
|
|
22
|
+
return "", true
|
|
23
|
+
}
|
|
24
|
+
return filepath.ToSlash(rel), true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// IsExcluded reports whether any exclude pattern matches path. The root ("") is never excluded.
|
|
28
|
+
func IsExcluded(path string, excludes []string) bool {
|
|
29
|
+
if path == "" {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
for _, pattern := range excludes {
|
|
33
|
+
if matchesGlob(path, pattern) {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// MatchesPatterns applies include alternatives and exclude precedence; the root itself is always
|
|
41
|
+
// in scope, filters describe its descendants.
|
|
42
|
+
func MatchesPatterns(path string, includes, excludes []string) bool {
|
|
43
|
+
if path == "" {
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
if IsExcluded(path, excludes) {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
if len(includes) == 0 {
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
for _, pattern := range includes {
|
|
53
|
+
if matchesGlob(path, pattern) {
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// MergeChange coalesces two consecutive changes to one path inside a batch. The empty string is
|
|
61
|
+
// "nothing pending" both as input and as the result (a transient path that came and went).
|
|
62
|
+
func MergeChange(previous, next ahptypes.ResourceChangeType) ahptypes.ResourceChangeType {
|
|
63
|
+
switch previous {
|
|
64
|
+
case "":
|
|
65
|
+
return next
|
|
66
|
+
case ahptypes.ResourceChangeTypeAdded:
|
|
67
|
+
if next == ahptypes.ResourceChangeTypeDeleted {
|
|
68
|
+
return ""
|
|
69
|
+
}
|
|
70
|
+
return ahptypes.ResourceChangeTypeAdded
|
|
71
|
+
case ahptypes.ResourceChangeTypeDeleted:
|
|
72
|
+
if next == ahptypes.ResourceChangeTypeDeleted {
|
|
73
|
+
return ahptypes.ResourceChangeTypeDeleted
|
|
74
|
+
}
|
|
75
|
+
return ahptypes.ResourceChangeTypeUpdated
|
|
76
|
+
}
|
|
77
|
+
if next == ahptypes.ResourceChangeTypeDeleted {
|
|
78
|
+
return ahptypes.ResourceChangeTypeDeleted
|
|
79
|
+
}
|
|
80
|
+
return ahptypes.ResourceChangeTypeUpdated
|
|
81
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
package svc
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/twin"
|
|
9
|
+
ahptypes "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// Twins of upstream test/resource-watch-policy.test.ts: deterministic, no OS events or clocks.
|
|
13
|
+
|
|
14
|
+
const (
|
|
15
|
+
cAdded = ahptypes.ResourceChangeTypeAdded
|
|
16
|
+
cUpdated = ahptypes.ResourceChangeTypeUpdated
|
|
17
|
+
cDeleted = ahptypes.ResourceChangeTypeDeleted
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
func TestWatchPathScopeAndFilters(t *testing.T) {
|
|
21
|
+
twin.Run(t, "resource-watch-policy", "normalizes descendants and rejects siblings and prefix lookalikes", func(t *testing.T) {
|
|
22
|
+
root, _ := filepath.Abs("workspace")
|
|
23
|
+
if rel, ok := RelativeWatchPath(root, root); !ok || rel != "" {
|
|
24
|
+
t.Fatalf("root: %q %v", rel, ok)
|
|
25
|
+
}
|
|
26
|
+
if rel, ok := RelativeWatchPath(root, filepath.Join(root, "nested/../file.txt")); !ok || rel != "file.txt" {
|
|
27
|
+
t.Fatalf("%q %v", rel, ok)
|
|
28
|
+
}
|
|
29
|
+
if rel, ok := RelativeWatchPath(root, filepath.Join(root, "nested/file.txt")); !ok || rel != "nested/file.txt" {
|
|
30
|
+
t.Fatalf("%q %v", rel, ok)
|
|
31
|
+
}
|
|
32
|
+
for _, path := range []string{filepath.Join(root, ".."), filepath.Join(root, "../sibling/file.txt"), root + "-other/file.txt"} {
|
|
33
|
+
if rel, ok := RelativeWatchPath(root, path); ok {
|
|
34
|
+
t.Errorf("%s must be out of scope, got %q", path, rel)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
twin.Run(t, "resource-watch-policy", "keeps the watched root visible regardless of filters", func(t *testing.T) {
|
|
40
|
+
if IsExcluded("", []string{"**"}) {
|
|
41
|
+
t.Error("the root must never be excluded")
|
|
42
|
+
}
|
|
43
|
+
if !MatchesPatterns("", []string{"*.md"}, []string{"**"}) {
|
|
44
|
+
t.Error("the root must always match")
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
twin.Run(t, "resource-watch-policy", "applies include alternatives and exclude precedence to every path", func(t *testing.T) {
|
|
49
|
+
cases := []struct {
|
|
50
|
+
path string
|
|
51
|
+
includes, exclud []string
|
|
52
|
+
want bool
|
|
53
|
+
}{
|
|
54
|
+
{"file.txt", nil, nil, true},
|
|
55
|
+
{"nested/file.md", []string{"**/*.md"}, nil, true},
|
|
56
|
+
{"nested/file.txt", []string{"**/*.md"}, nil, false},
|
|
57
|
+
{"file.ts", []string{"**/*.md", "**/*.ts"}, nil, true},
|
|
58
|
+
{"nested/file.md", []string{"**/*.md"}, []string{"nested/**"}, false},
|
|
59
|
+
{"node_modules/pkg/index.js", nil, []string{"**/node_modules/**"}, false},
|
|
60
|
+
{"nested/node_modules/pkg/index.js", nil, []string{"**/node_modules/**"}, false},
|
|
61
|
+
{"node_modules", nil, []string{"**/node_modules/**"}, true},
|
|
62
|
+
{".git/config", nil, []string{"**/.git/**"}, false},
|
|
63
|
+
{"src/.hidden.ts", []string{"**/*.ts"}, nil, false},
|
|
64
|
+
{"src/.hidden.ts", []string{"**/.*.ts"}, nil, true},
|
|
65
|
+
}
|
|
66
|
+
for _, c := range cases {
|
|
67
|
+
if got := MatchesPatterns(c.path, c.includes, c.exclud); got != c.want {
|
|
68
|
+
t.Errorf("%s includes=%v excludes=%v: got %v, want %v", c.path, c.includes, c.exclud, got, c.want)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
twin.Run(t, "resource-watch-policy", "does not prune a directory just because its children alone match the include filter", func(t *testing.T) {
|
|
74
|
+
if MatchesPatterns("src", []string{"**/*.ts"}, nil) {
|
|
75
|
+
t.Error("src alone does not match **/*.ts")
|
|
76
|
+
}
|
|
77
|
+
if IsExcluded("src", nil) {
|
|
78
|
+
t.Error("nothing is excluded without patterns")
|
|
79
|
+
}
|
|
80
|
+
if !MatchesPatterns("src/file.ts", []string{"**/*.ts"}, nil) {
|
|
81
|
+
t.Error("src/file.ts must match")
|
|
82
|
+
}
|
|
83
|
+
if !IsExcluded("src/generated", []string{"**/generated"}) {
|
|
84
|
+
t.Error("src/generated must be excluded")
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func checkTransitions(t *testing.T, previous ahptypes.ResourceChangeType, expected [3]ahptypes.ResourceChangeType) {
|
|
90
|
+
t.Helper()
|
|
91
|
+
for i, change := range []ahptypes.ResourceChangeType{cAdded, cUpdated, cDeleted} {
|
|
92
|
+
if got := MergeChange(previous, change); got != expected[i] {
|
|
93
|
+
t.Errorf("%q then %s: got %q, want %q", previous, change, got, expected[i])
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func TestWatchBatchTransitions(t *testing.T) {
|
|
99
|
+
twin.Run(t, "resource-watch-policy", "coalesces empty followed by each native event type", func(t *testing.T) {
|
|
100
|
+
checkTransitions(t, "", [3]ahptypes.ResourceChangeType{cAdded, cUpdated, cDeleted})
|
|
101
|
+
})
|
|
102
|
+
twin.Run(t, "resource-watch-policy", "coalesces added followed by each native event type", func(t *testing.T) {
|
|
103
|
+
checkTransitions(t, cAdded, [3]ahptypes.ResourceChangeType{cAdded, cAdded, ""})
|
|
104
|
+
})
|
|
105
|
+
twin.Run(t, "resource-watch-policy", "coalesces updated followed by each native event type", func(t *testing.T) {
|
|
106
|
+
checkTransitions(t, cUpdated, [3]ahptypes.ResourceChangeType{cUpdated, cUpdated, cDeleted})
|
|
107
|
+
})
|
|
108
|
+
twin.Run(t, "resource-watch-policy", "coalesces deleted followed by each native event type", func(t *testing.T) {
|
|
109
|
+
checkTransitions(t, cDeleted, [3]ahptypes.ResourceChangeType{cUpdated, cUpdated, cDeleted})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
twin.Run(t, "resource-watch-policy", "handles replacement and transient paths across longer sequences", func(t *testing.T) {
|
|
113
|
+
cases := []struct {
|
|
114
|
+
sequence []ahptypes.ResourceChangeType
|
|
115
|
+
want ahptypes.ResourceChangeType
|
|
116
|
+
}{
|
|
117
|
+
{[]ahptypes.ResourceChangeType{cAdded, cUpdated, cDeleted}, ""},
|
|
118
|
+
{[]ahptypes.ResourceChangeType{cAdded, cDeleted, cAdded}, cAdded},
|
|
119
|
+
{[]ahptypes.ResourceChangeType{cDeleted, cAdded, cDeleted}, cDeleted},
|
|
120
|
+
{[]ahptypes.ResourceChangeType{cDeleted, cAdded, cDeleted, cAdded}, cUpdated},
|
|
121
|
+
{[]ahptypes.ResourceChangeType{cUpdated, cDeleted, cAdded}, cUpdated},
|
|
122
|
+
}
|
|
123
|
+
for _, c := range cases {
|
|
124
|
+
var got ahptypes.ResourceChangeType
|
|
125
|
+
for _, change := range c.sequence {
|
|
126
|
+
got = MergeChange(got, change)
|
|
127
|
+
}
|
|
128
|
+
if got != c.want {
|
|
129
|
+
t.Errorf("%s: got %q, want %q", fmt.Sprint(c.sequence), got, c.want)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func TestGlobMatcher(t *testing.T) {
|
|
136
|
+
// Additions: the matcher is ours (Node's path.matchesGlob is not in the standard library).
|
|
137
|
+
cases := []struct {
|
|
138
|
+
path, pattern string
|
|
139
|
+
want bool
|
|
140
|
+
}{
|
|
141
|
+
{"a/b.ts", "a/*.ts", true}, {"a/b/c.ts", "a/*.ts", false}, {"a", "a/**", false}, {"a/b", "a/**", true},
|
|
142
|
+
{"a/x/b", "a/**/b", true}, {"a/b", "a/**/b", true}, {"x.md", "*.{md,txt}", true}, {"x.go", "*.{md,txt}", false},
|
|
143
|
+
{"file1", "file[0-9]", true}, {"filex", "file[!0-9]", true}, {"file1", "file[!0-9]", false}, {"ab", "a?", true},
|
|
144
|
+
{".env", "*", false}, {".env", ".*", true}, {"a/.git/x", "a/**", false}, {"a[b", "a[b", true},
|
|
145
|
+
}
|
|
146
|
+
for _, c := range cases {
|
|
147
|
+
if got := matchesGlob(c.path, c.pattern); got != c.want {
|
|
148
|
+
t.Errorf("matchesGlob(%q, %q) = %v, want %v", c.path, c.pattern, got, c.want)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|