feat(task-board): idempotent task creation via external_ref#414
feat(task-board): idempotent task creation via external_ref#414dimakis wants to merge 4 commits into
Conversation
Tasks can now carry an optional externalRef field (e.g. "pr_shepherd:dimakis/centaur#42"). The server checks for an existing task with the same ref before creating, returning 200 + deduplicated flag instead of a duplicate 201. A partial unique index on external_ref (WHERE NOT NULL) provides the DB-level safety net for concurrent requests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 3 issue(s) (1 warning).
frontend/src/types/task.ts
Clean, well-tested feature. The main gap is that frontend Task types in frontend/src/types/task.ts and packages/client/src/slices/tasks.ts were not updated to include externalRef, creating a server/client type mismatch.
- 🟡 regressions: The server-side
Taskinterface now includesexternalRef: string | null, but the frontend Task type (frontend/src/types/task.tsline 48) and the client package Task type (packages/client/src/slices/tasks.tsline 39) were not updated. The server API returnsexternalRefin all task responses, creating a type mismatch. Any future frontend code referencingtask.externalRefwill get a TypeScript error despite the field being present at runtime.[fixable]
server/api-schemas.ts
Clean, well-tested feature. The main gap is that frontend Task types in frontend/src/types/task.ts and packages/client/src/slices/tasks.ts were not updated to include externalRef, creating a server/client type mismatch.
- 🔵 unsafe_assumptions (L126):
externalRefis validated asz.string().optional()— this accepts empty strings. An empty string is non-NULL in SQLite, so it would hit the UNIQUE constraint on a second task withexternalRef: "". Consider adding.min(1)(liketitleon line 116) to prevent ambiguous empty-string refs.[fixable]
server/__tests__/task-store.test.ts
Clean, well-tested feature. The main gap is that frontend Task types in frontend/src/types/task.ts and packages/client/src/slices/tasks.ts were not updated to include externalRef, creating a server/client type mismatch.
- 🔵 missing_tests: No migration test for the new
external_refcolumn. The existing migration test (line 512) verifies workflow columns survive reopen, but doesn't assertexternalRefis populated correctly after migration 2 runs. Following the same pattern, a test should create a DB, close it, reopen (triggering migration 2), and verifytask.externalRefisnullfor pre-existing rows.[fixable]
| gateConfig: z.record(z.string(), z.unknown()).optional(), | ||
| maxRetries: z.number().min(0).optional(), | ||
| templateId: z.string().optional(), | ||
| externalRef: z.string().optional(), |
There was a problem hiding this comment.
🔵 unsafe_assumptions: externalRef is validated as z.string().optional() — this accepts empty strings. An empty string is non-NULL in SQLite, so it would hit the UNIQUE constraint on a second task with externalRef: "". Consider adding .min(1) (like title on line 116) to prevent ambiguous empty-string refs. [fixable]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Centaur review: empty strings are non-NULL in SQLite and would collide on the unique index. Add .min(1) validation to match the title field pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Centaur ReviewFound 4 issue(s) (2 warning).
|
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 2 issue(s).
server/__tests__/task-routes.test.ts
Clean, well-structured PR. Schema, migration, idempotency guard, and test coverage are all solid. Two minor test gaps — an empty-string rejection regression test and a migration-specific test — would harden the feature further.
- 🔵 missing_tests (L194): No test that
externalRef: ""(empty string) is rejected with 400. Commit 0ece048 specifically added.min(1)to fix this — a regression test would prevent that validator from being accidentally weakened.[fixable]
server/__tests__/task-store.test.ts
Clean, well-structured PR. Schema, migration, idempotency guard, and test coverage are all solid. Two minor test gaps — an empty-string rejection regression test and a migration-specific test — would harden the feature further.
- 🔵 missing_tests (L545): No migration test for the new
external_refcolumn. The existingmigrates existing database to add workflow columnstest covers migration 1 by creating a DB without the column and reopening it. A parallel test for migration 2 would ensure the ALTER TABLE + CREATE UNIQUE INDEX migration runs correctly on pre-existing databases.[fixable]
| expect(first.status).toBe(201); | ||
| expect(second.status).toBe(201); | ||
| expect(first.body.task.id).not.toBe(second.body.task.id); | ||
| }); |
There was a problem hiding this comment.
🔵 missing_tests: No test that externalRef: "" (empty string) is rejected with 400. Commit 0ece048 specifically added .min(1) to fix this — a regression test would prevent that validator from being accidentally weakened. [fixable]
| store.create({ title: 'B' }); | ||
| const roots = store.listRoots(); | ||
| expect(roots.length).toBeGreaterThanOrEqual(2); | ||
| }); |
There was a problem hiding this comment.
🔵 missing_tests: No migration test for the new external_ref column. The existing migrates existing database to add workflow columns test covers migration 1 by creating a DB without the column and reopening it. A parallel test for migration 2 would ensure the ALTER TABLE + CREATE UNIQUE INDEX migration runs correctly on pre-existing databases. [fixable]
Centaur review: cover the .min(1) validation with a 400 test, and verify migration 2 adds external_ref to pre-existing DBs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Centaur ReviewFound 4 issue(s) (1 warning).
|
Centaur ReviewFound 3 issue(s) (1 warning).
|
Centaur ReviewFound 4 issue(s) (2 warning).
|
Centaur ReviewFound 4 issue(s) (1 warning).
|
Summary
external_refcolumn to task store with a partial unique index (WHERE NOT NULL)POST /api/taskschecks for existing task byexternalRefbefore creating — returns 200 +deduplicated: trueinstead of a duplicate 201Companion PR: dimakis/mgmt — wires
external_refinto PR Shepherd goal creation andmitzo_client.create_task()Test plan
🤖 Generated with Claude Code