Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions nodes/src/nodes/tool_google_workspace/drive/IInstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,13 @@ def _files(self):
description=(
'Check the Google Drive connection and verify that the granted OAuth scopes cover the '
"node's configured access tier. Call this when a Drive operation fails with a scope or "
'permission error. Returns connection_ok: true when the required scopes are present.'
'permission error. Returns connection_ok: true when the API probe succeeds and the required scopes are present.'
),
input_schema={'type': 'object', 'properties': {}, 'required': []},
)
def check_connection(self, args: dict) -> dict:
"""Check Drive connection status and whether granted OAuth scopes cover the access tier. Read-only."""
return self._check_connection_impl()
return self._check_connection_impl(probe=lambda s: execute(s.about().get(fields='user')))

# =======================================================================
# FILES — read
Expand Down
4 changes: 2 additions & 2 deletions nodes/src/nodes/tool_google_workspace/gmail/IInstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,13 @@ def _id_list(args: dict, key: str, op: str) -> list[str]:
"Check the Gmail connection status and verify that the OAuth token's granted scopes "
"cover the node's configured access tier. Call this when a Gmail operation fails with "
'a scope or permission error, or before attempting settings-level operations for the '
'first time. Returns connection_ok: true when all required scopes are present.'
'first time. Returns connection_ok: true when the API probe succeeds and all required scopes are present.'
),
input_schema={'type': 'object', 'properties': {}, 'required': []},
)
def check_connection(self, args: dict) -> dict:
"""Check Gmail connection status and whether granted OAuth scopes cover the configured access tier. Read-only."""
return self._check_connection_impl()
return self._check_connection_impl(probe=lambda s: execute(s.users().getProfile(userId=USER_ID)))

# =======================================================================
# MESSAGES — read
Expand Down
11 changes: 11 additions & 0 deletions nodes/test/tool_google_workspace/drive/test_drive.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ def drives(self):
def changes(self):
return _Node(self, 'changes')

def about(self):
return _Node(self, 'about')

def call_for(self, op):
"""Return the kwargs of the FIRST recorded call to terminal method ``op``."""
return next((kw for n, kw in self.calls if n == op), None)
Expand Down Expand Up @@ -729,6 +732,14 @@ def test_check_connection_reports_ok():
assert out['connection_ok'] is True
assert out['access'] == 'write'
assert any('drive' in s for s in out['requiredScopes'])
assert inst.IGlobal.service.call_for('get')['fields'] == 'user'


def test_check_connection_reports_drive_api_probe_failure():
inst = _make(results={'get': RuntimeError('Drive API disabled')})
out = inst.check_connection({})
assert out['connection_ok'] is False
assert 'Drive API disabled' in out['error']


# ---------------------------------------------------------------------------
Expand Down
20 changes: 18 additions & 2 deletions nodes/test/tool_google_workspace/gmail/test_gmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,9 +569,9 @@ def test_thread_modify_requires_labels():
# ---------------------------------------------------------------------------


def _inst_with_token(access_tier: str, token: dict | None) -> 'IInstance.IInstance':
def _inst_with_token(access_tier: str, token: dict | None, results: dict | None = None) -> 'IInstance.IInstance':
"""Return an IInstance whose IGlobal.glb simulates a persisted user token."""
inst = make_inst(access_tier)
inst = make_inst(access_tier, results=results)
token_json = json.dumps(token) if token is not None else ''
inst.IGlobal.glb = types.SimpleNamespace(logicalType='tool_gmail', connConfig={})
inst.IGlobal._token_cfg = {'authType': 'user', 'userToken': token_json}
Expand Down Expand Up @@ -609,10 +609,26 @@ def test_check_connection_ok_with_full_scope(monkeypatch):
_patch_config(monkeypatch, inst)
result = inst.check_connection({})
assert result['connection_ok']
assert inst.IGlobal.service.call_for('getProfile')['userId'] == 'me'
assert result.get('missingScopes', []) == []
assert result['authType'] == 'user'


def test_check_connection_reports_gmail_api_probe_failure(monkeypatch):
"""check_connection reports disabled/unreachable Gmail API as not OK."""
mocks = Path(__file__).resolve().parents[2] / 'mocks'
monkeypatch.syspath_prepend(str(mocks))
inst = _inst_with_token(
'settings',
{'access_token': 'tok', 'scope': 'https://mail.google.com/'},
results={'getProfile': RuntimeError('Gmail API disabled')},
)
_patch_config(monkeypatch, inst)
result = inst.check_connection({})
assert result['connection_ok'] is False
assert 'Gmail API disabled' in result['error']


def test_check_connection_readonly_satisfied_by_modify(monkeypatch):
"""A token granted gmail.modify covers the readonly tier (scope implication)."""
mocks = Path(__file__).resolve().parents[2] / 'mocks'
Expand Down
Loading