@andypai/agent-kanban 0.6.1 → 0.6.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andypai/agent-kanban",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Agent-friendly kanban board CLI. Manage tasks via bash commands, parse structured JSON output.",
5
5
  "homepage": "https://github.com/abpai/agent-kanban#readme",
6
6
  "repository": {
@@ -37,6 +37,54 @@ describe('handleRequest', () => {
37
37
  expect(result.mutated).toBe(false)
38
38
  })
39
39
 
40
+ test('returns the error envelope for a malformed JSON body', async () => {
41
+ const req = new Request('http://localhost/api/tasks', {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: '{ not valid json',
45
+ })
46
+ const result = await handleRequest(provider, req)
47
+ const body = (await result.response.json()) as {
48
+ ok: boolean
49
+ error: { code: string; message: string }
50
+ }
51
+
52
+ expect(result.response.status).toBe(400)
53
+ expect(result.mutated).toBe(false)
54
+ expect(body.ok).toBe(false)
55
+ expect(body.error.code).toBe('INVALID_REQUEST_BODY')
56
+ })
57
+
58
+ test('still returns MISSING_ARGUMENT when a valid body omits a required field', async () => {
59
+ const req = new Request('http://localhost/api/tasks', {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify({ description: 'no title' }),
63
+ })
64
+ const result = await handleRequest(provider, req)
65
+ const body = (await result.response.json()) as {
66
+ ok: boolean
67
+ error: { code: string }
68
+ }
69
+
70
+ expect(result.response.status).toBe(400)
71
+ expect(result.mutated).toBe(false)
72
+ expect(body.error.code).toBe('MISSING_ARGUMENT')
73
+ })
74
+
75
+ test('rejects an invalid limit query parameter through the envelope', async () => {
76
+ const req = new Request('http://localhost/api/tasks?limit=-5', { method: 'GET' })
77
+ const result = await handleRequest(provider, req)
78
+ const body = (await result.response.json()) as {
79
+ ok: boolean
80
+ error: { code: string }
81
+ }
82
+
83
+ expect(result.response.status).toBe(400)
84
+ expect(body.ok).toBe(false)
85
+ expect(body.error.code).toBe('INVALID_ARGUMENT')
86
+ })
87
+
40
88
  test('marks successful task creation as mutated', async () => {
41
89
  const req = new Request('http://localhost/api/tasks', {
42
90
  method: 'POST',
@@ -1,6 +1,6 @@
1
1
  import { beforeEach, describe, expect, test } from 'bun:test'
2
2
  import { Database } from 'bun:sqlite'
3
- import { initSchema, seedDefaultColumns, resolveColumn } from '../db'
3
+ import { initSchema, seedDefaultColumns, resolveColumn, bulkMoveAll } from '../db'
4
4
  import { LocalProvider } from '../providers/local'
5
5
  import { ErrorCode, KanbanError } from '../errors'
6
6
 
@@ -62,6 +62,30 @@ describe('local provider conflict detection', () => {
62
62
  expect(v2.version).toBe('2')
63
63
  })
64
64
 
65
+ test('bulkMoveAll bumps the task version so a stale expectedVersion conflicts', async () => {
66
+ const created = await provider.createTask({ title: 'T1', column: 'backlog' })
67
+ expect(created.version).toBe('0')
68
+
69
+ const { moved } = bulkMoveAll(db, 'backlog', 'in-progress')
70
+ expect(moved).toBe(1)
71
+
72
+ // The move is a real mutation, so the version must change.
73
+ const after = await provider.getTask(created.id)
74
+ expect(after.version).toBe('1')
75
+
76
+ let err: unknown
77
+ try {
78
+ await provider.updateTask(created.id, {
79
+ title: 'stale',
80
+ expectedVersion: created.version ?? undefined,
81
+ })
82
+ } catch (e) {
83
+ err = e
84
+ }
85
+ expect(err).toBeInstanceOf(KanbanError)
86
+ expect((err as KanbanError).code).toBe(ErrorCode.CONFLICT)
87
+ })
88
+
65
89
  // moveTask(id, column) intentionally has no expectedVersion parameter, so moves
66
90
  // are last-write-wins by design (drag-and-drop has no loaded version to check).
67
91
  // This locks in that contract so a move is never rejected for a stale version.