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.
Files changed (62) hide show
  1. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/PKG-INFO +1 -1
  2. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/setup.py +1 -1
  3. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/__init__.py +1 -1
  4. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/app.py +0 -4
  5. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/exceptions.py +1 -0
  6. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/helpers.py +39 -21
  7. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/models.py +0 -3
  8. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/SKILL.md +10 -6
  9. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli.egg-info/PKG-INFO +1 -1
  10. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_agent_hints_and_cli.py +5 -8
  11. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_agent_skill_commands_context.py +4 -4
  12. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_cli_mock.py +1 -5
  13. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_phase1_improvements.py +7 -40
  14. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_setting_parser.py +8 -23
  15. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/MANIFEST.in +0 -0
  16. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/README.md +0 -0
  17. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/pyproject.toml +0 -0
  18. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/scripts/regression_test.py +0 -0
  19. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/setup.cfg +0 -0
  20. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/__main__.py +0 -0
  21. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/audit.py +0 -0
  22. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/auth_providers.py +0 -0
  23. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/__init__.py +0 -0
  24. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/auth.py +0 -0
  25. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/catalog.py +0 -0
  26. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/data.py +0 -0
  27. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/job.py +0 -0
  28. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/meta.py +0 -0
  29. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/odps.py +0 -0
  30. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/backend/query.py +0 -0
  31. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/cache.py +0 -0
  32. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/cli.py +0 -0
  33. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/config.py +0 -0
  34. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/masking.py +0 -0
  35. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/output.py +0 -0
  36. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/setting_parser.py +0 -0
  37. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  38. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/nohup.out +0 -0
  39. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
  40. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/references/command-patterns.md +0 -0
  41. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
  42. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/references/migrate-from-odpscmd.md +0 -0
  43. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  44. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/skills/references/setup-install.md +0 -0
  45. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/store.py +0 -0
  46. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli/utils.py +0 -0
  47. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli.egg-info/SOURCES.txt +0 -0
  48. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  49. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  50. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli.egg-info/requires.txt +0 -0
  51. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/src/maxc_cli.egg-info/top_level.txt +0 -0
  52. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_cache.py +0 -0
  53. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_catalog.py +0 -0
  54. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_compat.py +0 -0
  55. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_e2e_smoke.py +0 -0
  56. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_error_self_correction.py +0 -0
  57. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_external_auth.py +0 -0
  58. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_integration.py +0 -0
  59. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_integration_real.py +0 -0
  60. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_job_improvements.py +0 -0
  61. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_masking.py +0 -0
  62. {maxc_cli-0.1.8 → maxc_cli-0.1.9}/tests/test_query_auto_promote.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Agent-native MaxCompute CLI for external coding agents
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.8
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
9
9
 
10
10
  setup(
11
11
  name="maxc-cli",
12
- version="0.1.8",
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",
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.1.8"
5
+ __version__ = "0.1.9"
@@ -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
- return PermissionDeniedError(
1010
- f"Failed to list projects: missing Read permission on project {project_name}.",
1011
- suggestion=f"Verify that your account has `odps:Read` on project {project_name}, or contact the project owner.",
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
- return PermissionDeniedError(
1017
- f"Failed to list schemas: missing Read permission on project {target}.",
1018
- suggestion=f"Verify that your account has `odps:Read` on project {target}.",
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
- return PermissionDeniedError(
1023
- f"Failed to read project info: missing Read permission on project {project_name}.",
1024
- suggestion=f"Verify that your account has `odps:Read` on project {project_name}.",
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
- return PermissionDeniedError(
1029
- f"Operation failed: missing required permission on table {table_name}.",
1030
- suggestion=f"Verify that your account has the required permission on table {table_name}, or contact the project owner.",
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
- return PermissionDeniedError(
1035
- f"Operation failed: missing required permission on project {project_name}.",
1036
- suggestion=f"Verify that your account has the required permission on project {project_name}, or contact the project owner.",
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.actions[]` for structured navigation. Each action has `id`, `title`, `command`. Use `actions[].id` for stable program logic. Do not execute `next_actions` strings directly they may have broken shell quoting; construct commands yourself from the action `id` and context.
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 blindlyverify 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
- "actions": [
173
- {"id": "job.wait", "command": "maxc job wait 2026... --json"},
174
- {"id": "job.status", "command": "maxc job status 2026... --json"}
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.wait` or `job.status` using the `job_id`.
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 | Run `auth can-i --table <t> --operation SELECT --json` to verify; switch account if needed |
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` |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Agent-native MaxCompute CLI for external coding agents
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.8
@@ -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 test_agent_hints_render_executable_commands_with_action_ids() -> 'None':
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
- assert payload["command_id"] == "query.cost"
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
- assert payload["command_id"] == "query.cost"
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
- assert payload["command_id"] == "auth.whoami"
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
- assert payload["command_id"] == "cache.build"
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 give exit code 2; any other code means argparse accepted --mode
198
- assert code != 2, "--mode should not cause argparse error"
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
- # Should not be an argparse error
205
- assert code != 2, "--mode cost should not cause argparse error"
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"]["action_ids"] == ["auth.login", "auth.login-external"]
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 test_serialization_derives_next_actions_and_action_ids(self):
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 "actions" in hints
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 action_ids, actions[].id, command_id, and next_actions are consistent."""
414
+ """Verify next_actions and command formatting are consistent."""
419
415
 
420
- def test_action_ids_match_actions(self):
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 d["action_ids"] == [a["id"] for a in d["actions"]]
422
+ assert all(isinstance(s, str) for s in d["next_actions"])
427
423
 
428
- def test_next_actions_match_commands(self):
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 test_command_id_consistent_with_action_vocabulary(self):
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 READ_ONLY_VIOLATION maps to agent_hints with --force mention."""
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
- with patch("maxc_cli.cli.MaxCApp", return_value=mock_app):
198
- exit_code = run(
199
- ["query", "CREATE TABLE t (id BIGINT)", "--json"],
200
- stdout=stdout,
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"] == "READ_ONLY_VIOLATION"
206
- hints = output.get("agent_hints", {})
207
- # READ_ONLY_VIOLATION hints should contain a warning about server-side read-only
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