diff --git a/README.md b/README.md index 15f9a49..052c3b5 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ SDK: **[laravel/ai](https://laravel.com/docs/ai-sdk)** (`BaseAgent` + tools). We ``` GET /health POST /api/ask Bearer KIT_PEER_TOKEN {"message":"..."} +POST /api/assign Bearer KIT_PEER_TOKEN {"issue":"...","chair":"kit","brief":"..."} POST /api/webhooks/mattermost token=KIT_WEBHOOK_TOKEN → queues ReplyOnMattermost php artisan queue:work --queue=kit php artisan kit:ask "what's in the catalog?" diff --git a/app/Http/Controllers/AssignController.php b/app/Http/Controllers/AssignController.php new file mode 100644 index 0000000..8201386 --- /dev/null +++ b/app/Http/Controllers/AssignController.php @@ -0,0 +1,88 @@ +bearerToken(); + if ($expected === '' || ! hash_equals($expected, $got)) { + return response()->json(['error' => 'unauthorized'], 401); + } + + $issue = trim((string) $request->input('issue', '')); + if ($issue === '') { + return response()->json(['error' => 'issue required'], 422); + } + + $chair = strtolower(trim((string) $request->input('chair', 'kit'))); + if (! in_array($chair, ['kit', 'feel', 'bench'], true)) { + $chair = 'kit'; + } + + $number = $this->issueNumber($issue); + $id = $this->boardId($issue, $number, (string) $request->input('brief', '')); + $brief = trim((string) $request->input('brief', '')); + + $item = $board->upsert($id, [ + 'state' => 'queued', + 'lifecycle' => 'queued', + 'owner' => $chair, + 'issue' => $number, + 'hops' => 0, + 'note' => $brief !== '' ? $brief : 'assigned '.$issue, + ]); + + $this->hallwayAck($mattermost, $id, $issue, $chair); + + return response()->json([ + 'ok' => true, + 'board_id' => $id, + 'item' => $item, + ]); + } + + private function issueNumber(string $issue): int + { + if (preg_match('/#(\d+)/', $issue, $m) === 1) { + return (int) $m[1]; + } + if (preg_match('/\/issues\/(\d+)/', $issue, $m) === 1) { + return (int) $m[1]; + } + + return (int) $issue; + } + + private function boardId(string $issue, int $number, string $brief): string + { + $hay = strtolower($issue.' '.$brief); + foreach (['rider', 'hero-ebike', 'ranch-7620'] as $catalog) { + if (str_contains($hay, $catalog)) { + return $catalog; + } + } + + return $number > 0 ? 'issue-'.$number : 'assign'; + } + + private function hallwayAck(Client $mattermost, string $id, string $issue, string $chair): void + { + $hallway = (string) config('kit.mattermost.hallway_id'); + $dm = (string) config('kit.mattermost.dm_id'); + if ($hallway === '' || $hallway === $dm) { + return; + } + + $short = $number = $this->issueNumber($issue); + $ref = $short > 0 ? '#'.$short : $issue; + $mattermost->post($hallway, 'queued '.$id.' ← '.$ref.' ('.$chair.')'); + } +} diff --git a/routes/api.php b/routes/api.php index 924d473..90dcd40 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,10 +1,12 @@ $path, + 'kit.peer_token' => 'test-peer', + 'kit.mattermost.hallway_id' => 'hall-1', + 'kit.mattermost.dm_id' => 'dm-1', + ]); + $this->boardPath = $path; +}); + +afterEach(function () { + @unlink($this->boardPath); +}); + +test('assign refuses a bad bearer', function () { + $this->postJson('/api/assign', ['issue' => 'https://github.com/the-shit/bikes-v2/issues/12']) + ->assertUnauthorized(); +}); + +test('assign requires an issue', function () { + $this->withToken('test-peer') + ->postJson('/api/assign', ['brief' => 'no issue']) + ->assertStatus(422); +}); + +test('assign queues a board row and does not speak as the mouth', function () { + $mm = Mockery::mock(Client::class); + $mm->shouldReceive('post')->once()->with('hall-1', 'queued rider ← #12 (kit)'); + $this->app->instance(Client::class, $mm); + + $this->withToken('test-peer') + ->postJson('/api/assign', [ + 'issue' => 'https://github.com/the-shit/bikes-v2/issues/12', + 'chair' => 'kit', + 'brief' => 'rider stills', + ]) + ->assertOk() + ->assertJsonPath('ok', true) + ->assertJsonPath('board_id', 'rider'); + + $row = collect(app(Board::class)->read()['items'])->firstWhere('id', 'rider'); + expect($row['lifecycle'])->toBe('queued') + ->and($row['owner'])->toBe('kit') + ->and($row['issue'])->toBe(12); +}); + +test('assign skips mattermost when hallway is the Jordan DM', function () { + config(['kit.mattermost.hallway_id' => 'dm-1']); + $mm = Mockery::mock(Client::class); + $mm->shouldReceive('post')->never(); + $this->app->instance(Client::class, $mm); + + $this->withToken('test-peer') + ->postJson('/api/assign', [ + 'issue' => 'https://github.com/the-shit/kit/issues/5', + 'chair' => 'kit', + ]) + ->assertOk() + ->assertJsonPath('board_id', 'issue-5'); +});