@shadowob/connector 1.1.3-dev.251

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.
@@ -0,0 +1,159 @@
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ ROOT = Path(__file__).resolve().parents[1]
5
+ if str(ROOT) not in sys.path:
6
+ sys.path.insert(0, str(ROOT))
7
+
8
+ import adapter
9
+
10
+
11
+ def test_env_enablement_is_flat(monkeypatch):
12
+ monkeypatch.setenv('SHADOW_BASE_URL', 'https://shadow.example.com/api')
13
+ monkeypatch.setenv('SHADOW_TOKEN', 'tok')
14
+ monkeypatch.setenv('SHADOW_CHANNEL_IDS', 'c1,c2')
15
+ monkeypatch.setenv('SHADOW_HOME_CHANNEL', 'c3')
16
+ monkeypatch.setenv('SHADOW_MENTION_ONLY', 'true')
17
+ monkeypatch.setenv('SHADOW_AGENT_ID', 'agent-1')
18
+ monkeypatch.setenv('SHADOW_HEARTBEAT_INTERVAL_SECONDS', '15')
19
+ monkeypatch.setenv(
20
+ 'SHADOW_SLASH_COMMANDS_JSON',
21
+ '[{"name":"demo","description":"Demo command"}]',
22
+ )
23
+
24
+ seed = adapter._env_enablement()
25
+
26
+ assert seed['base_url'] == 'https://shadow.example.com/api'
27
+ assert seed['token'] == 'tok'
28
+ assert seed['channel_ids'] == ['c1', 'c2', 'c3']
29
+ assert seed['mention_only'] is True
30
+ assert seed['agent_id'] == 'agent-1'
31
+ assert seed['heartbeat_interval_seconds'] == '15'
32
+ assert seed['slash_commands'][0]['name'] == 'demo'
33
+ assert seed['home_channel']['chat_id'] == 'c3'
34
+ assert 'extra' not in seed
35
+
36
+
37
+ def test_check_requirements_allows_dynamic_remote_config_without_channel(monkeypatch):
38
+ monkeypatch.setenv('SHADOW_BASE_URL', 'https://shadow.example.com')
39
+ monkeypatch.setenv('SHADOW_TOKEN', 'tok')
40
+ monkeypatch.delenv('SHADOW_CHANNEL_IDS', raising=False)
41
+ monkeypatch.delenv('SHADOW_CHANNEL_ID', raising=False)
42
+ monkeypatch.delenv('SHADOW_HOME_CHANNEL', raising=False)
43
+ monkeypatch.delenv('SHADOW_SERVER_IDS', raising=False)
44
+ monkeypatch.delenv('SHADOW_AUTO_DISCOVER_CHANNELS', raising=False)
45
+
46
+ assert adapter.check_requirements() is True
47
+
48
+
49
+ def test_env_enablement_does_not_require_static_agent_or_channel(monkeypatch):
50
+ monkeypatch.setenv('SHADOW_BASE_URL', 'https://shadow.example.com')
51
+ monkeypatch.setenv('SHADOW_TOKEN', 'tok')
52
+ monkeypatch.delenv('SHADOW_CHANNEL_IDS', raising=False)
53
+ monkeypatch.delenv('SHADOW_CHANNEL_ID', raising=False)
54
+ monkeypatch.delenv('SHADOW_HOME_CHANNEL', raising=False)
55
+ monkeypatch.delenv('SHADOW_AGENT_ID', raising=False)
56
+
57
+ seed = adapter._env_enablement()
58
+
59
+ assert seed['base_url'] == 'https://shadow.example.com'
60
+ assert seed['token'] == 'tok'
61
+ assert 'channel_ids' not in seed
62
+ assert 'agent_id' not in seed
63
+
64
+
65
+ def test_remote_config_entries_filter_listen_policy():
66
+ remote_config = {
67
+ 'servers': [
68
+ {
69
+ 'id': 'server-1',
70
+ 'name': 'Server',
71
+ 'slug': 'server',
72
+ 'channels': [
73
+ {'id': 'listen-1', 'name': 'general', 'policy': {'listen': True}},
74
+ {'id': 'skip-1', 'name': 'quiet', 'policy': {'listen': False}},
75
+ ],
76
+ }
77
+ ]
78
+ }
79
+
80
+ entries = adapter._remote_listen_channel_entries(remote_config)
81
+
82
+ assert [entry[0] for entry in entries] == ['listen-1']
83
+ assert entries[0][1]['serverId'] == 'server-1'
84
+
85
+
86
+ def test_slash_command_prompt_and_interactive_block():
87
+ commands = [
88
+ {
89
+ 'name': 'deploy',
90
+ 'aliases': ['ship'],
91
+ 'description': 'Deploy something',
92
+ 'body': 'Run the deployment workflow.',
93
+ 'interaction': {
94
+ 'kind': 'form',
95
+ 'prompt': 'Choose a target',
96
+ 'fields': [{'id': 'target', 'kind': 'text', 'label': 'Target'}],
97
+ },
98
+ }
99
+ ]
100
+
101
+ match = adapter._slash_command_match('/ship prod', commands)
102
+
103
+ assert match is not None
104
+ assert match[1] == 'ship'
105
+ assert match[2] == 'prod'
106
+ prompt = adapter._format_slash_command_prompt('/ship prod', match)
107
+ assert 'Slash command /deploy was invoked.' in prompt
108
+ assert 'Run the deployment workflow.' in prompt
109
+ block = adapter._slash_interactive_block(match, 'message-1')
110
+ assert block['id'].endswith(':message-1')
111
+ assert block['kind'] == 'form'
112
+
113
+
114
+ def test_interactive_response_text_appends_shadow_context():
115
+ message = {
116
+ 'metadata': {
117
+ 'interactiveResponse': {
118
+ 'blockId': 'block-1',
119
+ 'actionId': 'approve',
120
+ 'value': 'yes',
121
+ 'values': {'comment': 'ok'},
122
+ }
123
+ }
124
+ }
125
+
126
+ text = adapter._interactive_response_text('Clicked approve', message)
127
+
128
+ assert 'Clicked approve' in text
129
+ assert '[Shadow interactive response]' in text
130
+ assert 'actionId: approve' in text
131
+ assert '"comment": "ok"' in text
132
+
133
+
134
+ def test_interactive_response_text_includes_source_followup_prompt():
135
+ message = {
136
+ 'metadata': {
137
+ 'interactiveResponse': {
138
+ 'sourceMessageId': 'source-1',
139
+ 'blockId': 'block-1',
140
+ 'actionId': 'ok',
141
+ 'value': 'ok',
142
+ }
143
+ }
144
+ }
145
+ source = {
146
+ 'content': 'Choose an action',
147
+ 'metadata': {
148
+ 'interactive': {
149
+ 'prompt': 'Confirm the action',
150
+ 'responsePrompt': 'Reply exactly DONE.',
151
+ }
152
+ },
153
+ }
154
+
155
+ text = adapter._interactive_response_text('', message, source)
156
+
157
+ assert 'sourceMessage: Choose an action' in text
158
+ assert 'sourcePrompt: Confirm the action' in text
159
+ assert 'followUpInstruction: Reply exactly DONE.' in text
@@ -0,0 +1,25 @@
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ ROOT = Path(__file__).resolve().parents[1]
5
+ if str(ROOT) not in sys.path:
6
+ sys.path.insert(0, str(ROOT))
7
+
8
+ from shadow_sdk import normalize_base_url, parse_bool, split_csv
9
+
10
+
11
+ def test_normalize_base_url_strips_api_suffix():
12
+ assert normalize_base_url('https://example.com/api') == 'https://example.com'
13
+ assert normalize_base_url('https://example.com/api/') == 'https://example.com'
14
+
15
+
16
+ def test_parse_bool():
17
+ assert parse_bool('true') is True
18
+ assert parse_bool('1') is True
19
+ assert parse_bool('off', True) is False
20
+ assert parse_bool(None, True) is True
21
+
22
+
23
+ def test_split_csv():
24
+ assert split_csv('a,b, c') == ['a', 'b', 'c']
25
+ assert split_csv(['a', '', 'b']) == ['a', 'b']
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@shadowob/connector",
3
+ "version": "1.1.3-dev.251",
4
+ "description": "Shadow connector helpers for OpenClaw, Hermes Agent, and cc-connect",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "require": "./dist/index.cjs",
9
+ "types": "./dist/index.d.ts",
10
+ "bin": {
11
+ "shadowob-connector": "./dist/cli.js"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "development": "./src/index.ts",
17
+ "import": "./dist/index.js",
18
+ "require": "./dist/index.cjs",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "hermes-shadowob-plugin",
25
+ "README.md"
26
+ ],
27
+ "keywords": [
28
+ "shadow",
29
+ "shadowob",
30
+ "openclaw",
31
+ "hermes-agent",
32
+ "cc-connect",
33
+ "connector"
34
+ ],
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/buggyblues/shadow",
39
+ "directory": "packages/connector"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.15.21",
43
+ "tsup": "^8.5.0",
44
+ "typescript": "^5.9.3",
45
+ "vitest": "^4.1.0"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "dev": "tsup --watch",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest"
56
+ }
57
+ }