maxc-cli 0.1.8__tar.gz → 0.1.9__tar.gz
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.
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/PKG-INFO +1 -1
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/setup.py +1 -1
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/app.py +0 -4
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/exceptions.py +1 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/helpers.py +39 -21
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/models.py +0 -3
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/SKILL.md +10 -6
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli.egg-info/PKG-INFO +1 -1
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_agent_hints_and_cli.py +5 -8
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_agent_skill_commands_context.py +4 -4
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_cli_mock.py +1 -5
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_phase1_improvements.py +7 -40
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_setting_parser.py +8 -23
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/MANIFEST.in +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/README.md +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/pyproject.toml +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/scripts/regression_test.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/setup.cfg +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/auth_providers.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/catalog.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/job.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/odps.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/query.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/cli.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/config.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/masking.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/output.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/setting_parser.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/nohup.out +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/references/command-patterns.md +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/references/migrate-from-odpscmd.md +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/references/partition-guide.md +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/utils.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli.egg-info/SOURCES.txt +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_cache.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_catalog.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_compat.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_error_self_correction.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_external_auth.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_integration.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_job_improvements.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_masking.py +0 -0
- {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_query_auto_promote.py +0 -0
|
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name="maxc-cli",
|
|
12
|
-
version="0.1.
|
|
12
|
+
version="0.1.9",
|
|
13
13
|
description="Agent-native MaxCompute CLI for external coding agents",
|
|
14
14
|
long_description=README.read_text(encoding="utf-8"),
|
|
15
15
|
long_description_content_type="text/markdown",
|
|
@@ -1884,10 +1884,6 @@ class MaxCApp:
|
|
|
1884
1884
|
f"Unable to access project `{project}`: {exc}",
|
|
1885
1885
|
suggestion="Verify the project name and that the current identity has access.",
|
|
1886
1886
|
) from exc
|
|
1887
|
-
else:
|
|
1888
|
-
warnings.append(
|
|
1889
|
-
"Project override was saved without remote validation because no authenticated backend session is active."
|
|
1890
|
-
)
|
|
1891
1887
|
override["project"] = project
|
|
1892
1888
|
changes.append(f"project set to `{project}`")
|
|
1893
1889
|
# Warn if session override project differs from the project saved in auth config
|
|
@@ -70,6 +70,7 @@ class MaxCError(Exception):
|
|
|
70
70
|
"""Return default recovery steps based on error code."""
|
|
71
71
|
_STEPS: 'dict[str, list[str]]' = {
|
|
72
72
|
"PERMISSION_DENIED": [
|
|
73
|
+
"If the current project does not end with _dev, you likely need the dev workspace (personal accounts usually only have dev access): maxc session set --project <project>_dev",
|
|
73
74
|
"Check the table and operation with: maxc auth can-i --table <table> --operation SELECT --json",
|
|
74
75
|
"Verify your project access with: maxc auth whoami --json",
|
|
75
76
|
"Contact your project administrator for access.",
|
|
@@ -997,6 +997,17 @@ def classify_sql_error(message: str) -> dict[str, Any]:
|
|
|
997
997
|
return {"error_type": "unknown"}
|
|
998
998
|
|
|
999
999
|
|
|
1000
|
+
def _dev_workspace_hint(project: 'str | None') -> 'str':
|
|
1001
|
+
"""Return a hint about switching to the _dev workspace if the project is not a dev workspace."""
|
|
1002
|
+
if project and not project.endswith("_dev"):
|
|
1003
|
+
return (
|
|
1004
|
+
f"Current project '{project}' is a production workspace. "
|
|
1005
|
+
f"Personal accounts usually only have access to the dev workspace (_dev). "
|
|
1006
|
+
f"Try switching: maxc session set --project {project}_dev"
|
|
1007
|
+
)
|
|
1008
|
+
return ""
|
|
1009
|
+
|
|
1010
|
+
|
|
1000
1011
|
def _build_permission_error(
|
|
1001
1012
|
message: 'str',
|
|
1002
1013
|
context: 'str',
|
|
@@ -1004,37 +1015,44 @@ def _build_permission_error(
|
|
|
1004
1015
|
table_name: 'str | None',
|
|
1005
1016
|
schema_name: 'str | None',
|
|
1006
1017
|
) -> 'PermissionDeniedError':
|
|
1007
|
-
"""Build permission-denied errors with more precise suggestions.
|
|
1018
|
+
"""Build permission-denied errors with more precise suggestions.
|
|
1019
|
+
|
|
1020
|
+
The original ODPS error (with RequestId, error code, etc.) is always
|
|
1021
|
+
preserved as the error message for diagnostics. Context-specific
|
|
1022
|
+
guidance goes into the suggestion field.
|
|
1023
|
+
"""
|
|
1024
|
+
dev_hint = _dev_workspace_hint(project_name)
|
|
1025
|
+
|
|
1008
1026
|
if context == "list_projects" and project_name:
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
suggestion=f"
|
|
1012
|
-
)
|
|
1027
|
+
suggestion = f"Verify that your account has `odps:Read` on project {project_name}, or contact the project owner."
|
|
1028
|
+
if dev_hint:
|
|
1029
|
+
suggestion = f"{dev_hint}\n{suggestion}"
|
|
1030
|
+
return PermissionDeniedError(message, suggestion=suggestion)
|
|
1013
1031
|
|
|
1014
1032
|
if context == "list_schemas":
|
|
1015
1033
|
target = project_name or "the current project"
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
suggestion=f"
|
|
1019
|
-
)
|
|
1034
|
+
suggestion = f"Verify that your account has `odps:Read` on project {target}."
|
|
1035
|
+
if dev_hint:
|
|
1036
|
+
suggestion = f"{dev_hint}\n{suggestion}"
|
|
1037
|
+
return PermissionDeniedError(message, suggestion=suggestion)
|
|
1020
1038
|
|
|
1021
1039
|
if context == "get_project_info" and project_name:
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
suggestion=f"
|
|
1025
|
-
)
|
|
1040
|
+
suggestion = f"Verify that your account has `odps:Read` on project {project_name}."
|
|
1041
|
+
if dev_hint:
|
|
1042
|
+
suggestion = f"{dev_hint}\n{suggestion}"
|
|
1043
|
+
return PermissionDeniedError(message, suggestion=suggestion)
|
|
1026
1044
|
|
|
1027
1045
|
if table_name:
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
suggestion=f"
|
|
1031
|
-
)
|
|
1046
|
+
suggestion = f"Verify that your account has the required permission on table {table_name}, or contact the project owner."
|
|
1047
|
+
if dev_hint:
|
|
1048
|
+
suggestion = f"{dev_hint}\n{suggestion}"
|
|
1049
|
+
return PermissionDeniedError(message, suggestion=suggestion)
|
|
1032
1050
|
|
|
1033
1051
|
if project_name:
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
suggestion=f"
|
|
1037
|
-
)
|
|
1052
|
+
suggestion = f"Verify that your account has the required permission on project {project_name}, or contact the project owner."
|
|
1053
|
+
if dev_hint:
|
|
1054
|
+
suggestion = f"{dev_hint}\n{suggestion}"
|
|
1055
|
+
return PermissionDeniedError(message, suggestion=suggestion)
|
|
1038
1056
|
|
|
1039
1057
|
return PermissionDeniedError(message)
|
|
1040
1058
|
|
|
@@ -38,8 +38,6 @@ class AgentHints:
|
|
|
38
38
|
def to_dict(self) -> 'dict[str, Any]':
|
|
39
39
|
payload: 'dict[str, Any]' = {}
|
|
40
40
|
if self.actions:
|
|
41
|
-
payload["actions"] = [a.to_dict() for a in self.actions]
|
|
42
|
-
payload["action_ids"] = [a.id for a in self.actions]
|
|
43
41
|
payload["next_actions"] = [a.command for a in self.actions]
|
|
44
42
|
if self.warnings:
|
|
45
43
|
payload["warnings"] = self.warnings
|
|
@@ -64,7 +62,6 @@ class Envelope:
|
|
|
64
62
|
payload = {
|
|
65
63
|
"version": self.version,
|
|
66
64
|
"command": command,
|
|
67
|
-
"command_id": self.command,
|
|
68
65
|
"status": self.status,
|
|
69
66
|
"data": data,
|
|
70
67
|
"metadata": self.metadata,
|
|
@@ -100,7 +100,7 @@ See [references/migrate-from-odpscmd.md](references/migrate-from-odpscmd.md) for
|
|
|
100
100
|
- `agent skill` returns the SKILL.md path and metadata.
|
|
101
101
|
- `agent install-skill <platform>` registers the skill with an Agent platform (claude-code, cursor, windsurf, codex, qwen, qoder, qoderwork). Idempotent; re-run after `pip install --upgrade` to update local skill files.
|
|
102
102
|
- Use normalized `data` shapes: `auth whoami` → `data.identity`, `query`/`job result` → `data.result`, `meta describe` → `data.table`, `data sample` → `data.sample`.
|
|
103
|
-
- Use `agent_hints.
|
|
103
|
+
- Use `agent_hints.next_actions[]` for suggested follow-up commands. Each entry is a ready-to-run CLI command string. Do not execute them blindly — verify the command makes sense for your current context first.
|
|
104
104
|
- Before exploring an unfamiliar project, ask the user which schema/table they need — do not iterate all schemas.
|
|
105
105
|
- When a query fails with `SQL_ERROR`, read `error.suggestion` before retrying. Do not retry the same SQL unchanged.
|
|
106
106
|
- For partitioned tables, always determine the partition value via `meta latest-partition` or `meta partitions` before querying.
|
|
@@ -169,16 +169,16 @@ When `--wait N` is exceeded, `status` is `pending` with a `job_id` in metadata:
|
|
|
169
169
|
"sql_executed": "SELECT ..."
|
|
170
170
|
},
|
|
171
171
|
"agent_hints": {
|
|
172
|
-
"
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
"next_actions": [
|
|
173
|
+
"maxc job wait 2026... --json",
|
|
174
|
+
"maxc job status 2026... --json"
|
|
175
175
|
],
|
|
176
176
|
"insights": ["Query promoted to async after 10s."]
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
Follow up with `job
|
|
181
|
+
Follow up with `maxc job wait` or `maxc job status` using the `job_id`.
|
|
182
182
|
|
|
183
183
|
### DDL/DML with --force
|
|
184
184
|
|
|
@@ -212,6 +212,8 @@ Standard flow for answering data questions:
|
|
|
212
212
|
- Column names with spaces or special characters must be backtick-escaped: `` `column name` ``.
|
|
213
213
|
- When filtering by column values, first check actual values with `data sample` or a `SELECT DISTINCT` query — don't guess enum values.
|
|
214
214
|
- For partitioned tables, always filter by partition column in WHERE (e.g. `WHERE ds = '20260415'`) to avoid full-table scans. Use `maxc meta partitions <table>` to discover available partitions.
|
|
215
|
+
- When the user is in a `_dev` workspace and needs to access tables from another project, SQL must use the full 3-tier `project.schema.table` format (e.g. `other_project.my_schema.my_table`), because `_dev` workspaces can only see tables within their own space by default.
|
|
216
|
+
- MaxCompute projects map to workspaces. Within the organization, workspaces are split into dev (`_dev` suffix) and production. Personal accounts usually lack production workspace permissions. If you hit a permission error, first check whether you need to switch to `<project>_dev`.
|
|
215
217
|
- Never log, echo, or include AK/SK in output — even in error context.
|
|
216
218
|
|
|
217
219
|
## Partition Query Strategy
|
|
@@ -288,6 +290,8 @@ See [references/maxcompute-sql-notes.md](references/maxcompute-sql-notes.md) for
|
|
|
288
290
|
| Assuming `meta describe` data is live | Cache source may be stale; check `metadata.source` field and `agent_hints.warnings` |
|
|
289
291
|
| Querying partitioned table without partition filter, or hardcoding/guessing partition values | Always run `meta latest-partition` first; use the exact returned value in WHERE (see Partition Query Strategy) |
|
|
290
292
|
| Assuming 2-tier naming (`project.table`) | Check if project uses 3-tier namespace with `meta list-schemas` |
|
|
293
|
+
| Using a production project name (without `_dev` suffix) with a personal account | Switch to the dev workspace: `maxc session set --project <project>_dev` |
|
|
294
|
+
| Using `schema.table` to access tables in another project from a `_dev` workspace | Use `project.schema.table` 3-tier format to specify the target project |
|
|
291
295
|
|
|
292
296
|
## Agent Anti-Patterns
|
|
293
297
|
|
|
@@ -311,7 +315,7 @@ When a command returns `status=failure`, inspect the `error.code` field to deter
|
|
|
311
315
|
| `TABLE_NOT_FOUND` | Table does not exist in the schema | Check `error.did_you_mean`; search with `meta search <name> --json` |
|
|
312
316
|
| `COLUMN_NOT_FOUND` | Column reference does not exist | Check `error.available` for valid columns; run `meta describe <table> --json` |
|
|
313
317
|
| `WRITE_OPERATION_REQUIRES_FORCE` | Write operation blocked by read-only mode | Read-only is enforced by design; use `--force` only if explicitly authorized |
|
|
314
|
-
| `PERMISSION_DENIED` | No access to the resource |
|
|
318
|
+
| `PERMISSION_DENIED` | No access to the resource | If the current project does NOT end with `_dev`, personal accounts likely need the dev workspace: `maxc session set --project <project>_dev`. Then verify with `auth can-i --table <t> --operation SELECT --json` |
|
|
315
319
|
| `SQL_ERROR` | SQL syntax or execution error | Fix the SQL; use `query explain` to validate syntax first |
|
|
316
320
|
| `COST_LIMIT_EXCEEDED` | Query cost exceeds `--cost-check` threshold | Lower the scan scope (add partition filters, reduce columns), or raise the threshold |
|
|
317
321
|
| `BACKEND_CONNECTION_ERROR` | Network or service unavailable | Retry after a delay; check endpoint with `auth whoami --json` |
|
|
@@ -16,7 +16,7 @@ from maxc_cli.exceptions import ValidationError
|
|
|
16
16
|
from maxc_cli.models import AgentHints, Envelope, action
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def
|
|
19
|
+
def test_agent_hints_render_executable_commands() -> 'None':
|
|
20
20
|
envelope = Envelope(
|
|
21
21
|
command="query.cost",
|
|
22
22
|
status="success",
|
|
@@ -34,9 +34,8 @@ def test_agent_hints_render_executable_commands_with_action_ids() -> 'None':
|
|
|
34
34
|
payload = envelope.to_dict()
|
|
35
35
|
|
|
36
36
|
assert payload["command"] == "query cost"
|
|
37
|
-
|
|
37
|
+
# command_id removed in v0.1.6+
|
|
38
38
|
assert payload["data"] == {"analysis": {"estimated_input_size_bytes": 0}}
|
|
39
|
-
assert payload["agent_hints"]["action_ids"] == ["query.explain", "query"]
|
|
40
39
|
assert payload["agent_hints"]["next_actions"] == [
|
|
41
40
|
"maxc query explain 'SELECT 1 AS one' --json",
|
|
42
41
|
"maxc query 'SELECT 1 AS one' --json",
|
|
@@ -73,7 +72,6 @@ def test_agent_hints_infer_table_query_and_pagination_commands() -> 'None':
|
|
|
73
72
|
"next_cursor": "eyJvIjoyMH0=",
|
|
74
73
|
},
|
|
75
74
|
}
|
|
76
|
-
assert payload["agent_hints"]["action_ids"] == ["query", "query.paginate", "meta.describe"]
|
|
77
75
|
assert payload["agent_hints"]["next_actions"] == [
|
|
78
76
|
"maxc query 'SELECT * FROM sales.orders LIMIT 20' --json",
|
|
79
77
|
"maxc query 'SELECT * FROM sales.orders LIMIT 20' --cursor eyJvIjoyMH0= --json",
|
|
@@ -136,7 +134,7 @@ def test_query_alias_routes_to_query_cost() -> 'None':
|
|
|
136
134
|
assert app.calls == [("cost", "SELECT 1 AS one", None)]
|
|
137
135
|
payload = json.loads(stdout.getvalue())
|
|
138
136
|
assert payload["command"] == "query cost"
|
|
139
|
-
|
|
137
|
+
# command_id removed in v0.1.6+
|
|
140
138
|
|
|
141
139
|
|
|
142
140
|
def test_query_alias_and_mode_flag_cannot_be_combined() -> 'None':
|
|
@@ -257,7 +255,7 @@ def test_auth_whoami_without_credentials_returns_guidance(tmp_path: 'Path', monk
|
|
|
257
255
|
payload = envelope.to_dict()
|
|
258
256
|
|
|
259
257
|
assert payload["command"] == "auth whoami"
|
|
260
|
-
|
|
258
|
+
# command_id removed in v0.1.6+
|
|
261
259
|
assert payload["data"]["identity"]["authenticated"] is False
|
|
262
260
|
assert payload["data"]["identity"]["configured"] is False
|
|
263
261
|
assert payload["data"]["auth_options"][0]["command"] == "auth login --from-env"
|
|
@@ -269,7 +267,6 @@ def test_meta_list_projects_hints_use_existing_commands(tmp_path: 'Path') -> 'No
|
|
|
269
267
|
envelope = app.meta_list_projects()
|
|
270
268
|
payload = envelope.to_dict()
|
|
271
269
|
|
|
272
|
-
assert payload["agent_hints"]["action_ids"] == ["session.set", "meta.list-schemas"]
|
|
273
270
|
assert payload["agent_hints"]["next_actions"] == [
|
|
274
271
|
"maxc session set --json",
|
|
275
272
|
"maxc meta list-schemas --json",
|
|
@@ -319,7 +316,7 @@ def test_cache_build_json_handler_emits_single_envelope() -> 'None':
|
|
|
319
316
|
assert app.calls == [(None, None, False, True)]
|
|
320
317
|
payload = json.loads(stdout.getvalue())
|
|
321
318
|
assert payload["command"] == "cache build"
|
|
322
|
-
|
|
319
|
+
# command_id removed in v0.1.6+
|
|
323
320
|
assert payload["status"] == "success"
|
|
324
321
|
stderr_text = args.stderr.getvalue()
|
|
325
322
|
assert "Fetching table list..." in stderr_text
|
|
@@ -194,15 +194,15 @@ class TestQueryModeDeprecation:
|
|
|
194
194
|
"""--mode run should still work for backward compatibility."""
|
|
195
195
|
config = _make_config(tmp_path)
|
|
196
196
|
code, payload, _ = _run_cmd(config, ["query", "SELECT 1", "--mode", "run", "--json"])
|
|
197
|
-
# argparse errors
|
|
198
|
-
assert
|
|
197
|
+
# argparse errors produce no JSON envelope; a non-empty payload means argparse accepted --mode
|
|
198
|
+
assert payload, "--mode should not cause argparse error (expected JSON envelope)"
|
|
199
199
|
|
|
200
200
|
def test_mode_cost_still_works(self, tmp_path):
|
|
201
201
|
"""--mode cost should still work and trigger deprecation warning."""
|
|
202
202
|
config = _make_config(tmp_path)
|
|
203
203
|
code, payload, _ = _run_cmd(config, ["query", "SELECT 1", "--mode", "cost", "--json"])
|
|
204
|
-
#
|
|
205
|
-
assert
|
|
204
|
+
# argparse errors produce no JSON envelope; a non-empty payload means argparse accepted --mode
|
|
205
|
+
assert payload, "--mode cost should not cause argparse error (expected JSON envelope)"
|
|
206
206
|
|
|
207
207
|
def test_mode_hidden_from_help(self, tmp_path):
|
|
208
208
|
"""--mode should be suppressed from --help output."""
|
|
@@ -144,7 +144,6 @@ def test_auth_login_can_create_new_explicit_config_without_validation(
|
|
|
144
144
|
|
|
145
145
|
assert code == 0
|
|
146
146
|
assert payload["command"] == "auth login"
|
|
147
|
-
assert payload["command_id"] == "auth.login"
|
|
148
147
|
assert payload["data"]["persistence"]["saved"] is True
|
|
149
148
|
assert payload["data"]["persistence"]["validated"] is False
|
|
150
149
|
assert payload["data"]["identity"]["identity_source"] == "config_file"
|
|
@@ -199,7 +198,6 @@ auth:
|
|
|
199
198
|
|
|
200
199
|
assert code == 0
|
|
201
200
|
assert payload["command"] == "auth whoami"
|
|
202
|
-
assert payload["command_id"] == "auth.whoami"
|
|
203
201
|
identity = payload["data"]["identity"]
|
|
204
202
|
assert identity["authenticated"] is True
|
|
205
203
|
assert identity["configured"] is True
|
|
@@ -290,7 +288,7 @@ auth:
|
|
|
290
288
|
assert identity["configured"] is True
|
|
291
289
|
assert identity["validation_status"] == "failed"
|
|
292
290
|
assert identity["identity_source"] == "config_file"
|
|
293
|
-
assert payload["agent_hints"]["
|
|
291
|
+
assert "maxc auth login" in str(payload["agent_hints"]["next_actions"])
|
|
294
292
|
assert any(
|
|
295
293
|
"failed to resolve remote whoami endpoint" in warning
|
|
296
294
|
for warning in payload["agent_hints"]["warnings"]
|
|
@@ -392,7 +390,6 @@ allowed_operations:
|
|
|
392
390
|
|
|
393
391
|
assert code == 1
|
|
394
392
|
assert payload["command"] == "cache status"
|
|
395
|
-
assert payload["command_id"] == "cache.status"
|
|
396
393
|
assert payload["status"] == "failure"
|
|
397
394
|
assert payload["error"]["code"] == "VALIDATION_ERROR"
|
|
398
395
|
|
|
@@ -422,7 +419,6 @@ allowed_operations:
|
|
|
422
419
|
|
|
423
420
|
assert code == 1
|
|
424
421
|
assert payload["command"] == "session set"
|
|
425
|
-
assert payload["command_id"] == "session.set"
|
|
426
422
|
assert payload["status"] == "failure"
|
|
427
423
|
assert payload["error"]["code"] == "VALIDATION_ERROR"
|
|
428
424
|
|
|
@@ -75,7 +75,7 @@ class TestActionFactory:
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
class TestAgentHintsWithActions:
|
|
78
|
-
def
|
|
78
|
+
def test_serialization_derives_next_actions(self):
|
|
79
79
|
hints = AgentHints(actions=[
|
|
80
80
|
SuggestedAction(
|
|
81
81
|
id="meta.describe",
|
|
@@ -89,13 +89,10 @@ class TestAgentHintsWithActions:
|
|
|
89
89
|
),
|
|
90
90
|
])
|
|
91
91
|
d = hints.to_dict()
|
|
92
|
-
assert d["action_ids"] == ["meta.describe", "data.sample"]
|
|
93
92
|
assert d["next_actions"] == [
|
|
94
93
|
"maxc meta describe schools --json",
|
|
95
94
|
"maxc data sample schools --json",
|
|
96
95
|
]
|
|
97
|
-
assert len(d["actions"]) == 2
|
|
98
|
-
assert d["actions"][0]["id"] == "meta.describe"
|
|
99
96
|
|
|
100
97
|
def test_envelope_renders_actions_through_agent_hints(self):
|
|
101
98
|
envelope = Envelope(
|
|
@@ -115,8 +112,7 @@ class TestAgentHintsWithActions:
|
|
|
115
112
|
)
|
|
116
113
|
payload = envelope.to_dict()
|
|
117
114
|
hints = payload["agent_hints"]
|
|
118
|
-
assert "
|
|
119
|
-
assert hints["action_ids"] == ["meta.describe"]
|
|
115
|
+
assert "next_actions" in hints
|
|
120
116
|
assert hints["next_actions"] == ["maxc meta describe <table_name> --json"]
|
|
121
117
|
|
|
122
118
|
|
|
@@ -415,24 +411,17 @@ class TestEnhancedClassifySqlError:
|
|
|
415
411
|
|
|
416
412
|
|
|
417
413
|
class TestConsistency:
|
|
418
|
-
"""Verify
|
|
414
|
+
"""Verify next_actions and command formatting are consistent."""
|
|
419
415
|
|
|
420
|
-
def
|
|
416
|
+
def test_next_actions_are_command_strings(self):
|
|
421
417
|
hints = AgentHints(actions=[
|
|
422
418
|
action("meta.describe", data={"table_name": "t"}),
|
|
423
419
|
action("data.sample", data={"table_name": "t"}),
|
|
424
420
|
])
|
|
425
421
|
d = hints.to_dict()
|
|
426
|
-
assert
|
|
422
|
+
assert all(isinstance(s, str) for s in d["next_actions"])
|
|
427
423
|
|
|
428
|
-
def
|
|
429
|
-
hints = AgentHints(actions=[
|
|
430
|
-
action("meta.describe", data={"table_name": "t"}),
|
|
431
|
-
])
|
|
432
|
-
d = hints.to_dict()
|
|
433
|
-
assert d["next_actions"] == [a["command"] for a in d["actions"]]
|
|
434
|
-
|
|
435
|
-
def test_command_id_uses_dot_notation(self):
|
|
424
|
+
def test_command_uses_space_notation(self):
|
|
436
425
|
envelope = Envelope(
|
|
437
426
|
command="meta.describe",
|
|
438
427
|
status="success",
|
|
@@ -440,33 +429,11 @@ class TestConsistency:
|
|
|
440
429
|
metadata={},
|
|
441
430
|
)
|
|
442
431
|
payload = envelope.to_dict()
|
|
443
|
-
assert payload["command_id"] == "meta.describe"
|
|
444
432
|
assert payload["command"] == "meta describe"
|
|
445
433
|
|
|
446
|
-
def
|
|
447
|
-
"""command_id and action IDs should use same vocabulary."""
|
|
448
|
-
envelope = Envelope(
|
|
449
|
-
command="query.cost",
|
|
450
|
-
status="success",
|
|
451
|
-
data={},
|
|
452
|
-
metadata={"project": "p"},
|
|
453
|
-
agent_hints=AgentHints(actions=[
|
|
454
|
-
action("query.explain"),
|
|
455
|
-
action("query"),
|
|
456
|
-
]),
|
|
457
|
-
)
|
|
458
|
-
payload = envelope.to_dict()
|
|
459
|
-
# command_id is dot notation
|
|
460
|
-
assert payload["command_id"] == "query.cost"
|
|
461
|
-
# action_ids are also dot notation
|
|
462
|
-
for aid in payload["agent_hints"]["action_ids"]:
|
|
463
|
-
assert "." in aid or aid in {"query"} # single-word commands are ok
|
|
464
|
-
|
|
465
|
-
def test_empty_actions_produces_no_hints_keys(self):
|
|
434
|
+
def test_empty_actions_produces_no_next_actions(self):
|
|
466
435
|
hints = AgentHints(actions=[], warnings=["test"])
|
|
467
436
|
d = hints.to_dict()
|
|
468
|
-
assert "actions" not in d
|
|
469
|
-
assert "action_ids" not in d
|
|
470
437
|
assert "next_actions" not in d
|
|
471
438
|
assert d["warnings"] == ["test"]
|
|
472
439
|
|
|
@@ -176,36 +176,21 @@ def test_translate_odps_error_type_error_not_readonly():
|
|
|
176
176
|
|
|
177
177
|
|
|
178
178
|
def test_cli_readonly_error_has_agent_hints():
|
|
179
|
-
"""Verify that
|
|
179
|
+
"""Verify that client-side write detection returns WRITE_OPERATION_REQUIRES_FORCE with hints."""
|
|
180
180
|
import io
|
|
181
181
|
import json
|
|
182
|
-
from unittest.mock import patch, MagicMock
|
|
183
182
|
|
|
184
183
|
from maxc_cli.cli import run
|
|
185
184
|
|
|
186
|
-
# Simulate a readonly error from the backend
|
|
187
|
-
from maxc_cli.exceptions import ReadOnlyError
|
|
188
|
-
|
|
189
|
-
mock_app = MagicMock()
|
|
190
|
-
mock_app.query.side_effect = ReadOnlyError(
|
|
191
|
-
"invalid statement in readonly mode",
|
|
192
|
-
suggestion="Use odpscmd for write operations.",
|
|
193
|
-
)
|
|
194
|
-
|
|
195
185
|
stdout = io.StringIO()
|
|
196
186
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
)
|
|
187
|
+
exit_code = run(
|
|
188
|
+
["query", "CREATE TABLE t (id BIGINT)", "--json"],
|
|
189
|
+
stdout=stdout,
|
|
190
|
+
)
|
|
202
191
|
|
|
203
192
|
assert exit_code != 0
|
|
204
193
|
output = json.loads(stdout.getvalue())
|
|
205
|
-
assert output["error"]["code"] == "
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
warnings = hints.get("warnings", [])
|
|
209
|
-
assert any("read-only" in w.lower() or "readonly" in w.lower() for w in warnings)
|
|
210
|
-
# Should also have structured actions
|
|
211
|
-
assert "actions" in hints
|
|
194
|
+
assert output["error"]["code"] == "WRITE_OPERATION_REQUIRES_FORCE"
|
|
195
|
+
assert output["error"]["recoverable"] is True
|
|
196
|
+
assert "--force" in output["error"].get("suggestion", "")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|