agent/php/tests/Feature/Api/Credits/RoutesTest.php
Snider dffdad8418 feat(api): implement §3 fleet+credits+subscription+sync+agent-auth routes (#848)
Additive-only — appended to php/Routes/api.php (existing routes
preserved). Existing /v1/fleet/{nodes,heartbeat,stats} +
/v1/agent/auth/provision left untouched.

New routes:
- /v1/agent/auth/register
- /v1/fleet/dispatch + /v1/fleet/stream
- /v1/credits/{balance,deduct,refund,ledger}
- /v1/subscription/{status,upgrade,cancel}
- /v1/agent/sync/{push,pull}

New controllers under php/Controllers/Api/{Fleet,Credits,Subscription,
Sync,AgentAuth}/. Reference FleetService/CreditService/SessionService
when available with fallbacks to current action/model layer (pre #849).

Pest Feature coverage under php/tests/Feature/Api/. pest skipped
(vendor binaries missing in sandbox).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=848
2026-04-25 05:43:51 +01:00

119 lines
3.7 KiB
PHP

<?php
// SPDX-License-Identifier: EUPL-1.2
declare(strict_types=1);
use Core\Mod\Agentic\Models\AgentApiKey;
use Core\Mod\Agentic\Models\CreditEntry;
use Core\Tenant\Models\Workspace;
beforeEach(function (): void {
require __DIR__.'/../../../../Routes/api.php';
});
function creditsRouteKey(
Workspace $workspace,
array $permissions = [AgentApiKey::PERM_CREDITS_READ, AgentApiKey::PERM_CREDITS_WRITE]
): AgentApiKey {
return createApiKey($workspace, 'Credits Route Key', $permissions);
}
test('credits balance route returns workspace totals', function (): void {
$workspace = createWorkspace();
$key = creditsRouteKey($workspace, [AgentApiKey::PERM_CREDITS_READ]);
CreditEntry::create([
'workspace_id' => $workspace->id,
'fleet_node_id' => null,
'task_type' => 'manual-refund',
'amount' => 30,
'balance_after' => 30,
]);
CreditEntry::create([
'workspace_id' => $workspace->id,
'fleet_node_id' => null,
'task_type' => 'manual-deduction',
'amount' => -10,
'balance_after' => 20,
]);
$response = $this
->withHeader('Authorization', 'Bearer '.$key->plainTextKey)
->getJson('/v1/credits/balance');
$response
->assertOk()
->assertJsonPath('data.workspace_id', $workspace->id)
->assertJsonPath('data.balance', 20)
->assertJsonPath('data.total_earned', 30)
->assertJsonPath('data.total_spent', 10)
->assertJsonPath('data.entries', 2);
});
test('credits deduct route records a negative ledger entry', function (): void {
$workspace = createWorkspace();
$key = creditsRouteKey($workspace, [AgentApiKey::PERM_CREDITS_WRITE]);
$response = $this
->withHeader('Authorization', 'Bearer '.$key->plainTextKey)
->postJson('/v1/credits/deduct', [
'amount' => 15,
'reason' => 'Manual moderation charge',
]);
$response
->assertCreated()
->assertJsonPath('data.amount', -15)
->assertJsonPath('data.balance_after', -15)
->assertJsonPath('data.task_type', 'manual-deduction');
});
test('credits refund route records a positive ledger entry', function (): void {
$workspace = createWorkspace();
$key = creditsRouteKey($workspace, [AgentApiKey::PERM_CREDITS_WRITE]);
$response = $this
->withHeader('Authorization', 'Bearer '.$key->plainTextKey)
->postJson('/v1/credits/refund', [
'amount' => 25,
'reason' => 'Manual goodwill refund',
]);
$response
->assertCreated()
->assertJsonPath('data.amount', 25)
->assertJsonPath('data.balance_after', 25)
->assertJsonPath('data.task_type', 'manual-refund');
});
test('credits ledger route returns the newest workspace entries first', function (): void {
$workspace = createWorkspace();
$key = creditsRouteKey($workspace, [AgentApiKey::PERM_CREDITS_READ]);
CreditEntry::create([
'workspace_id' => $workspace->id,
'fleet_node_id' => null,
'task_type' => 'manual-refund',
'amount' => 10,
'balance_after' => 10,
]);
CreditEntry::create([
'workspace_id' => $workspace->id,
'fleet_node_id' => null,
'task_type' => 'manual-deduction',
'amount' => -3,
'balance_after' => 7,
]);
$response = $this
->withHeader('Authorization', 'Bearer '.$key->plainTextKey)
->getJson('/v1/credits/ledger');
$response
->assertOk()
->assertJsonPath('total', 2)
->assertJsonPath('data.0.task_type', 'manual-deduction')
->assertJsonPath('data.0.balance_after', 7)
->assertJsonPath('data.1.task_type', 'manual-refund');
});