fix(0.43.5): metastore duplicate-name accepts 409 alongside legacy 500 - #318
Conversation
ottomansky
left a comment
There was a problem hiding this comment.
Review of #318 — fix(0.43.4): metastore duplicate-name accepts 409 alongside legacy 500
Generated by
kbagent-pr-reviewersubagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed viamake check, not duplicated here.
Summary
This PR extends MetastoreClient.post_item to recognise HTTP 409 Conflict (from
go-monorepo PR #513) as a duplicate-name signal alongside the legacy HTTP 500
workaround. The implementation is backward-compatible (500 path retained with the
original substring gate), the new 409 path correctly sets ErrorCode.ALREADY_EXISTS
with retryable=False, and no CLI command surface changes are introduced — so
OPERATION_REGISTRY, context.py, CLAUDE.md, and the hint definitions are
correctly untouched. One silent-drift finding: semantic-layer-workflow.md still
documents only the legacy 500 shape, so AI agents reading it will not know that
post-fix stacks emit 409 instead (they will still call kbagent semantic-layer add ...
correctly, but their mental model of the underlying contract will be stale).
No blocking issues were found.
Verdict: APPROVE — no blocking findings; one non-blocking silent-drift nit and
one cosmetic nit in the test.
Verdict
- Verdict: APPROVE
- Blocking findings: 0
- Non-blocking findings: 1
- Nits: 2
Blocking findings
(none)
Non-blocking findings
[NB-1] plugins/kbagent/skills/kbagent/references/semantic-layer-workflow.md:489 — stale "500" description needs updating to reflect 409
The file still reads:
- **Duplicate-name POST -> 500** with `"Failed to create meta object"`.
kbagent normalizes to `ErrorCode.ALREADY_EXISTS`.
After this PR ships, the primary signal on post-fix stacks is a 409, not a 500. Any
AI agent reading this workflow reference will build a mental model of the protocol
that is wrong for the majority of stacks going forward. Per CONTRIBUTING.md the
Plugin synchronization map row for gotchas.md and topic workflow files should be
updated whenever client-visible behavior changes.
Suggested fix: update the bullet to reflect both shapes, e.g.
- **Duplicate-name POST -> 409 Conflict** (post go-monorepo PR #513) with
`"Object with this name already exists in this project"`, or **500** with
`"Failed to create meta object"` on legacy stacks. kbagent normalizes both
into `ErrorCode.ALREADY_EXISTS` (since v0.43.4).
The (since v0.43.4) tag is required by CONTRIBUTING.md to prevent AI agents from
recommending the 409 behavior on stacks that haven't deployed the metastore fix yet
(though in this case the 409 behavior is transparent to callers — it's the server
that decides which code to return — so the operational risk is low; hence
NON-BLOCKING rather than BLOCKING).
Nits
-
[NIT-1]plugins/kbagent/agents/keboola-expert.md:185— the NEVER column still
readsbypasses the duplicate-name 500-to-ALREADY_EXISTS normalization. Now that
both 500 and 409 are normalized,500-to-ALREADY_EXISTSis technically incomplete;
duplicate-name-to-ALREADY_EXISTS normalizationwould be accurate going forward.
Low-risk cosmetic; does not affect correctness of the advice (the NEVER guidance
is still correct — raw metastore POSTs still bypass the normalization entirely). -
[NIT-2]tests/test_metastore_client.py:173-178— thetry/finallyaround
pytest.raisesis correct resource management, but the existing 500 tests at lines
193-201 and 211-218 use the same pattern. Consider a@pytest.fixturefor
MetastoreClient(...)+client.close()to reduce boilerplate. Minor; consistency
within the test class.
Verification log
gh pr view 318 --json title,body,files --repo padak/keboola_agent_cli→ 7 files,
+56/-14, conventionalfix:prefix ✓ (internal client behavior change, not
feat:, which is correct per CONTRIBUTING.md)git rev-parse --abbrev-ref HEAD→fix/metastore-409-conflict-mapping✓make check→ ruff clean, format clean, SKILL.md up-to-date, plugin.json in sync;
changelog-checkexits 1 (Missing changelog entries for: 0.44.0b1) — confirmed
pre-existing onupstream/mainby checking out upstream changelog and running the
same check; NOT introduced by this PR ✓uv run pytest tests/ -q --ignore=tests/test_e2e.py --ignore=tests/test_e2e_lineage_deep.py
→ 3394 passed, 26 skipped ✓uv run pytest tests/test_metastore_client.py -q→ 14 passed (13 existing + 1
newtest_duplicate_name_409_becomes_already_exists) ✓- 3-layer check: no
typer/clickimports added to services; nohttpxcalls in
commands; no formatter calls in clients → empty grep results ✓ - Convention check: no bare
except:, no rawerror_code="..."strings, no
print()in production code, no magic numbers added → empty grep results ✓ - Token discipline: only
token=TOKENtest variable reference in diff; no real
token pattern;mask_token()not needed (no error message surfaces the token) ✓ - Plugin synchronization map — no new CLI command registered;
@*_app.command()grep
on diff → empty ✓;OPERATION_REGISTRY,context.py,CLAUDE.md,hints/,
commands-reference.md,keboola-expert.md§2 matrix correctly untouched ✓ - Silent-drift hunt:
grep -rn 'Failed to create meta' . --include='*.md'→
semantic-layer-workflow.md:489still uses the legacy-only wording → NB-1 above keboola-expert.mdline 185 NEVER column: references500-to-ALREADY_EXISTS→
NIT-1 above- Backward-compat: 500 path retained with identical substring gate; unrelated-500
test (test_unrelated_500_passes_through) still registersMAX_RETRIESresponses
→ retryableAPI_ERRORbehavior confirmed ✓ - 409
retryable=Falsecontract:RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}inconstants.py:15— 409 absent → no retries before normalization fires ✓ - Behavior reproduction: manual execution not attempted (no test metastore available);
new testtest_duplicate_name_409_becomes_already_existsregisters exactly one 409
response (proving no retry) and assertsretryable=False— coverage is adequate
for the claim ✓ - Version bump:
pyproject.toml0.43.3 → 0.43.4;plugin.json+marketplace.jsonuv.lockall bumped ✓; changelog entry present and descriptive ✓
Open questions for the author
(none)
Addressing review feedbackPushed
No version bump — still 0.43.4, docs and test boilerplate only. Also ran a live e2e against the kbagent playground (
So backwards-compat is verified on a real metastore. Once go-monorepo #513 ships to this stack, the same e2e will hit the 409 branch (no retries) and surface the same |
`MetastoreClient.post_item` now normalises both HTTP 409 (post go-monorepo PR keboola#513) and the legacy HTTP 500 + `"Failed to create meta object"` body into `ErrorCode.ALREADY_EXISTS`. Before this change, the workaround only matched the 500 shape -- on a post-fix metastore a duplicate-name POST would bubble up as a generic `API_ERROR` 'API error 409 ...' instead of the clean ALREADY_EXISTS path the command layer special-cases. Side benefit: 409 is not in RETRYABLE_STATUS_CODES (constants.py), so duplicate-name POSTs against a post-fix metastore are no longer retried MAX_RETRIES times before the normalisation fires. The 500 substring check is retained so unrelated 500s still surface as retryable API_ERROR. PATCH on a missing UUID is also fixed upstream (returns 404 now), but `BaseHttpClient._handle_error` already maps 404 -> NOT_FOUND, so no client change needed for that path. Tests: new `test_duplicate_name_409_becomes_already_exists` registers a single 409 (asserting no retry) and verifies status_code=409, error_code=ALREADY_EXISTS, retryable=False, plus the canonical user- facing message. Existing 500 + unrelated-500 tests stay green. 14 passed in tests/test_metastore_client.py.
NB-1: `semantic-layer-workflow.md` duplicate-name bullet still documented only the legacy 500 shape -- updated to describe both 409 (post-fix, non-retryable) and 500 (legacy, retryable), with the `(since v0.43.4)` tag required by CONTRIBUTING.md so AI agents reading the reference don't recommend the 409 shape on stacks that haven't deployed the metastore fix yet. NIT-1: `keboola-expert.md` NEVER column referenced `500-to-ALREADY_EXISTS`, which is now incomplete (both 500 and 409 are normalised). Rewritten as `duplicate-name-to-ALREADY_EXISTS normalization (both 409 and legacy 500)` to keep the NEVER guidance accurate without stating the status codes inline. NIT-2: Replaced the per-test `MetastoreClient(...) + try/finally: client.close()` boilerplate in `TestDuplicateNameNormalization` with a module-scope `metastore_client` pytest fixture. 14 tests still pass. Scope intentionally narrowed to the class touched by this PR -- the broader test file uses the inline pattern and refactoring it all is a separate cleanup. No version bump (still 0.43.4); only docs + test boilerplate changed.
…e budget
The previous NIT-1 edit added "(both 409 and legacy 500)" to the NEVER
column, pushing keboola-expert.md from 59986 to 60021 bytes -- 21 over
the strict 60000-byte cap enforced by `tests/test_agent_prompt.py::
TestPilotAgentFile::test_agent_prompt_under_token_budget`. CI fail:
AssertionError: Agent prompt is 60021 bytes (~15005 tokens);
budget is 60000 bytes. Trim or split into specialists.
Drop the "(both 409 and legacy 500)" qualifier -- the wording
`duplicate-name-to-ALREADY_EXISTS normalization` already covers both
codes accurately without enumerating them inline. Net change: -5 bytes
vs the original 500-to-ALREADY_EXISTS line. The 409+500 explanation
lives in `semantic-layer-workflow.md` (NB-1 fix) where there's room.
The metastore 409+500 fix was originally bumped to 0.43.4, but that version shipped from PR keboola#309 (cascade-delete) one hour earlier. Bump goes to 0.43.5 instead. - pyproject.toml + make version-sync propagate 0.43.5 - changelog.py: new 0.43.5 entry for the 409+500 normalisation; 0.43.4 entry from keboola#309 untouched - semantic-layer-workflow.md: 'since v0.43.4' -> 'since v0.43.5' - gotchas.md: new '(since v0.43.5)' entry documenting that raw HTTP callers must handle BOTH 409 and 500 shapes for duplicate-name -- the gap CONTRIBUTING.md plugin sync map flagged No code-behaviour change vs the previously approved state.
85b9ded to
ae2404c
Compare
padak
left a comment
There was a problem hiding this comment.
Review of #318 — fix(0.43.5): metastore duplicate-name accepts 409 alongside legacy 500
Generated by
kbagent-pr-reviewersubagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed viamake check, not duplicated here.
Summary
This PR fixes a client-side normalization gap in MetastoreClient.post_item: after the upstream metastore fix (go-monorepo PR #513), the server now returns HTTP 409 Conflict for duplicate-name POSTs instead of the historical HTTP 500 quirk. The two-line condition extension is correct, well-justified, and backward-compatible during the rollout window. The version bump (0.43.4 -> 0.43.5), plugin-doc updates, new gotcha entry, and semantic-layer-workflow update are all present and correctly tagged with (since v0.43.5). The new test registers exactly one 409 response (asserting no retry), covers all key fields, and all 14 metastore tests plus the full 3396-test suite pass.
The single make check failure on changelog-check (Missing changelog entries for: 0.44.0b1) is pre-existing on main -- it predates this branch, is caused by a GitHub prerelease tag (v0.44.0b1) on feat/agent-cli-parity that has no entry in changelog.py, and is explicitly acknowledged in the PR description. This PR does not introduce or worsen it.
Verdict: APPROVE. No blocking findings.
Verdict
- Verdict: APPROVE
- Blocking findings: 0
- Non-blocking findings: 1
- Nits: 2
Blocking findings
(none)
Non-blocking findings
[NB-1] tests/test_metastore_client.py:231 — legacy 500 test missing status_code and retryable assertions
The new test_duplicate_name_409_becomes_already_exists (line 197) asserts status_code=409 and retryable=False. The existing test_duplicate_name_500_becomes_already_exists (line 217) asserts only error_code and message substrings, but does not assert status_code=500 or retryable=False. Since the PR preserves exc.status_code verbatim, the 500 path also sets retryable=False on the re-raised exception, but the test does not verify it. Adding the two assertions would lock in the contract symmetrically with the new 409 test -- a future refactor that accidentally made the 500 path retryable would otherwise go undetected.
Nits
-
[NIT-1]plugins/kbagent/agents/keboola-expert.md:114— the VERSION GATE list (§1 Rule 6) noteskbagent update --beta = 0.43.3+as the most recent metastore-adjacent line. Since v0.43.5 introduces no new CLI commands and the 409 normalization is transparent to callers, a VERSION GATE entry is not required by the checklist. Purely informational: if the team ever wants to document "safe to callsemantic-layer addagainst a post-PR#513 metastore stack" as a version gate, 0.43.5 is the anchor. Not a gap. -
[NIT-2]src/keboola_agent_cli/changelog.py:111— the changelog entry for0.43.5is unusually long (the entire body is a single list element running to ~700 words). By convention, other entries in the file are one-liners or two-liners; the verbosity is useful for internal history but may causekbagent changelogto render an awkward wall of text in human mode. Consider splitting into two shorter bullets, matching the 0.43.4 two-entry style already present in this PR's diff.
Verification log
gh pr view 318 --json title,body,files,additions,deletions,baseRefName,headRefName,labels,state→ 10 files, +97/-31,state=OPEN,headRefName=fix/metastore-409-conflict-mapping, conventionalfix(0.43.5):prefix matches a bug fix -- checkedgit rev-parse --abbrev-ref HEAD→fix/metastore-409-conflict-mapping-- working tree matches PR branch, proceedinggrep -E '^\+.*@.*_app\.command\(' /tmp/kbagent-pr-318.diff→ empty (no new CLI commands); Plugin synchronization map silent-drift surfaces skipped -- no command additions to track- Layer violation checks (typer in services, httpx in commands, formatter in clients) → all empty -- no layer violations
uv run pytest tests/test_metastore_client.py -v→ 14 passed (existing 13 + 1 newtest_duplicate_name_409_becomes_already_exists) -- exit 0uv run pytest tests/ -q --ignore=tests/test_e2e.py --ignore=tests/test_e2e_lineage_deep.py→ 3396 passed, 26 skipped, 16 warnings -- exit 0make check→ lint PASS, format PASS, SKILL.md PASS, plugin.json PASS, changelog-check FAIL (Missing changelog entries for: 0.44.0b1). Verified this failure also fires onmainHEAD (ab4ba91) and at the merge-base -- pre-existing, introduced whengh release create v0.44.0b1 --prereleasewas run againstfeat/agent-cli-paritywithout a correspondingchangelog.pykey. Not introduced by this PR.- Convention checks (magic numbers, raw error_code strings, bare except, print() in production, token exposure) → all clean
plugins/kbagent/skills/kbagent/references/gotchas.md:124→ new entry## Metastore duplicate-name POST returns 409 OR 500 -- both map to ALREADY_EXISTS (since v0.43.5)-- version tag present and correctplugins/kbagent/skills/kbagent/references/semantic-layer-workflow.md:492→since v0.43.5tag present in the "Quick reminders" sectionplugins/kbagent/agents/keboola-expert.mddiff → one-line update in the Tool Selection Matrix "Add a metric..." row: removes the stale "500-to-ALREADY_EXISTS" wording, replaces with generic "duplicate-name-to-ALREADY_EXISTS" -- correct- E2E test coverage: no new CLI commands introduced, so no new E2E test is required per CONTRIBUTING.md -- the existing
semantic-layerE2E tests cover the code path - Behavior reproduction: could not run live against a post-go-monorepo-PR#513 metastore stack (no such stack available in the local environment). The unit test suite registers a single mock 409 and verifies the full normalized exception shape -- this is the appropriate substitute. Marked as unverified runtime behavior; needs author confirmation that the live 409 normalizes correctly against a deployed fix stack.
Open questions for the author
- The
make checkchangelog-checkfailure exists identically onmain(caused by thev0.44.0b1GitHub prerelease tag onfeat/agent-cli-parityhaving nochangelog.pykey). Is there a plan to add a"0.44.0b1"entry tochangelog.pybefore that branch merges, or to configuregenerate_changelog.pyto skip prerelease tags that live on non-mainbranches? The current state means every PR rebased on main will surface the same spurious CI failure.
…on legacy 500 duplicate-name test kbagent-pr-reviewer NB-1 flagged a symmetry gap: the new 409 test asserts status_code and retryable explicitly, the existing 500 test did not. Both paths take the same KeboolaApiError(retryable=False, status_code=<orig>) construction, so the 500 path quietly relies on coverage that is not pinned. Two assertions added; behaviour unchanged. Why: symmetric test coverage makes regressions on either path equally loud. The next change to the duplicate-name branch will fail-fast on whichever path it broke instead of silently slipping through the legacy 500 test.
Review findings disposition (kbagent-pr-reviewer iter-1)
PR title also updated to |
Problem
MetastoreClient.post_itemhas a workaround for the metastore's historical "duplicate name → HTTP 500 'Failed to create meta object'" quirk that normalises it toErrorCode.ALREADY_EXISTS. keboola/go-monorepo#513 fixes the upstream bug so the metastore now returns a proper HTTP 409 Conflict with the message"Object with this name already exists in this project".The current
post_itemworkaround only matchesstatus_code == 500and a substring of the legacy message. Against a post-fix metastore deployment:BaseHttpClient's generic error path → returnsErrorCode.API_ERRORwith message"API error 409 from ...: Object with this name already exists ..."instead of the cleanALREADY_EXISTSpath that command-layer error renderers and exit-code mapping rely on.RETRYABLE_STATUS_CODES(constants.py:15) so today every duplicate-name POST eatsMAX_RETRIESround-trips before the workaround even fires. 409 is not retryable, so once the metastore fix is live, this PR also tightens the retry cost from 3 round-trips down to 1.Change
Two-line condition extension in
metastore_client.py:post_item:POST /api/v1/repository/{type}(by construction a uniqueness violation; seeservices/metastore/api/handlers/repository_errors.goin the go-monorepo PR — the only constraint name the handler matches in the create path isConstraintObjectTypeName).API_ERRORand doesn't get miscategorised as a name collision.Backwards-compatible across the rollout window — works against stacks with the metastore fix and stacks without it.
PATCH on a missing UUID is also fixed upstream (returns 404 now), but
BaseHttpClient._handle_erroralready maps404 → ErrorCode.NOT_FOUNDathttp_base.py:249, so no client change is needed for that path.Docstring on
metastore_client.pyupdated to describe both server-side shapes.Test plan
uv run pytest tests/test_metastore_client.py -q→ 14 passed (existing 13 + 1 new)make version-syncpropagated 0.43.4 toplugin.jsonandmarketplace.jsontest_duplicate_name_409_becomes_already_existsregisters a single 409 (asserts no retry), verifiesstatus_code=409,error_code=ALREADY_EXISTS,retryable=False, and the canonical user-facing messagetest_duplicate_name_500_becomes_already_exists+test_unrelated_500_passes_throughstay green (legacy / unrelated paths unchanged)Coordination
This PR is safe to merge before keboola/go-monorepo#513 lands — the 500 path is retained, so behaviour on stacks that haven't deployed the metastore fix is unchanged. After both PRs are merged + deployed, the retry-cost improvement also applies.
Notes
make changelog-checkflagsMissing changelog entries for: 0.44.0b1on upstream/main and on this branch identically. The 0.44.0b1 tag pre-exists without a changelog entry; not introduced by this PR.