2
0

TermWriteAsAiModelTest.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. <?php
  2. use App\Models\AiModel;
  3. use App\Models\DhammaTerm;
  4. use Illuminate\Foundation\Testing\RefreshDatabase;
  5. uses(RefreshDatabase::class);
  6. /**
  7. * 术语的写入链路与句子一致:人类签出 channel 的 access token,AI 模型带着
  8. * 它以自己的身份建/改术语。这里的核心同样是署名——editor_uid 必须是模型。
  9. */
  10. /** 人类签出 channel access token,并取出模型身份 token。 */
  11. function termWriteTokens($test, string $human, string $channel, AiModel $model): array
  12. {
  13. $accessToken = $test->postJson('/api/v2/access-token', [
  14. 'payload' => [[
  15. 'res_type' => 'channel',
  16. 'res_id' => $channel,
  17. 'power' => 'edit',
  18. 'book' => 0,
  19. ]],
  20. ], authHeader($human))
  21. ->assertOk()
  22. ->assertJsonPath('data.count', 1)
  23. ->json('data.rows.0.token');
  24. $modelToken = $test->getJson("/api/v2/ai-model-token/{$model->uid}", authHeader($human))
  25. ->assertOk()
  26. ->json('data.token');
  27. return [$accessToken, $modelToken];
  28. }
  29. it('creates a channel term attributed to the ai model', function () {
  30. $human = makeStudio('tester');
  31. $channel = makeChannel($human);
  32. $model = AiModel::factory()->ownedBy($human)->create(['name' => 'claude-opus-5']);
  33. [$accessToken, $modelToken] = termWriteTokens($this, $human, $channel, $model);
  34. $this->postJson('/api/v2/terms', [
  35. 'word' => 'satipaṭṭhāna',
  36. 'meaning' => '念处',
  37. 'other_meaning' => '念住',
  38. 'note' => '四念处之念处。',
  39. 'tag' => 'abhidhamma',
  40. 'channel' => $channel,
  41. 'access_token' => $accessToken,
  42. ], ['Authorization' => 'Bearer '.$modelToken])
  43. ->assertOk()
  44. ->assertJsonPath('data.word', 'satipaṭṭhāna');
  45. $saved = DhammaTerm::where('word', 'satipaṭṭhāna')->first();
  46. expect($saved)->not->toBeNull();
  47. expect($saved->meaning)->toBe('念处');
  48. expect($saved->channal)->toBe($channel);
  49. // 核心断言:署名归模型,而不是发起操作的人类
  50. expect($saved->editor_uid)->toBe($model->uid);
  51. expect($saved->editor_uid)->not->toBe($human);
  52. // owner 仍是 channel 所属 studio
  53. expect($saved->owner)->toBe($human);
  54. });
  55. it('refuses to create a term without an access token', function () {
  56. $human = makeStudio('tester');
  57. $channel = makeChannel($human);
  58. $model = AiModel::factory()->ownedBy($human)->create();
  59. $modelToken = $this->getJson("/api/v2/ai-model-token/{$model->uid}", authHeader($human))
  60. ->json('data.token');
  61. $this->postJson('/api/v2/terms', [
  62. 'word' => 'satipaṭṭhāna',
  63. 'meaning' => '念处',
  64. 'channel' => $channel,
  65. ], ['Authorization' => 'Bearer '.$modelToken])
  66. ->assertStatus(403);
  67. expect(DhammaTerm::count())->toBe(0);
  68. });
  69. it('refuses an access token issued for a different channel', function () {
  70. $human = makeStudio('tester');
  71. $channel = makeChannel($human, 'mine');
  72. $otherChannel = makeChannel(makeStudio('someone-else'), 'not mine');
  73. $model = AiModel::factory()->ownedBy($human)->create();
  74. [$accessToken, $modelToken] = termWriteTokens($this, $human, $channel, $model);
  75. $this->postJson('/api/v2/terms', [
  76. 'word' => 'satipaṭṭhāna',
  77. 'meaning' => '念处',
  78. 'channel' => $otherChannel,
  79. 'access_token' => $accessToken,
  80. ], ['Authorization' => 'Bearer '.$modelToken])
  81. ->assertStatus(403);
  82. expect(DhammaTerm::count())->toBe(0);
  83. });
  84. it('updates a term as the ai model, leaving unsubmitted fields untouched', function () {
  85. $human = makeStudio('tester');
  86. $channel = makeChannel($human);
  87. $model = AiModel::factory()->ownedBy($human)->create();
  88. [$accessToken, $modelToken] = termWriteTokens($this, $human, $channel, $model);
  89. $guid = $this->postJson('/api/v2/terms', [
  90. 'word' => 'satipaṭṭhāna',
  91. 'meaning' => '念处',
  92. 'note' => '原有的注解',
  93. 'tag' => 'abhidhamma',
  94. 'channel' => $channel,
  95. 'access_token' => $accessToken,
  96. ], ['Authorization' => 'Bearer '.$modelToken])
  97. ->assertOk()
  98. ->json('data.guid');
  99. $createTime = DhammaTerm::find($guid)->create_time;
  100. // 只提交 meaning:note/tag 必须原样保留
  101. $this->putJson("/api/v2/terms/{$guid}", [
  102. 'meaning' => '念住',
  103. 'access_token' => $accessToken,
  104. ], ['Authorization' => 'Bearer '.$modelToken])
  105. ->assertOk()
  106. ->assertJsonPath('data.meaning', '念住');
  107. $saved = DhammaTerm::find($guid);
  108. expect($saved->meaning)->toBe('念住');
  109. expect($saved->word)->toBe('satipaṭṭhāna');
  110. expect($saved->note)->toBe('原有的注解');
  111. expect($saved->tag)->toBe('abhidhamma');
  112. expect($saved->editor_uid)->toBe($model->uid);
  113. // create_time 是创建时刻,改动不该刷新它
  114. expect($saved->create_time)->toBe($createTime);
  115. });
  116. it('refuses to update a term with no access token', function () {
  117. $human = makeStudio('tester');
  118. $channel = makeChannel($human);
  119. $model = AiModel::factory()->ownedBy($human)->create();
  120. [$accessToken, $modelToken] = termWriteTokens($this, $human, $channel, $model);
  121. $guid = $this->postJson('/api/v2/terms', [
  122. 'word' => 'satipaṭṭhāna',
  123. 'meaning' => '念处',
  124. 'channel' => $channel,
  125. 'access_token' => $accessToken,
  126. ], ['Authorization' => 'Bearer '.$modelToken])->json('data.guid');
  127. $this->putJson("/api/v2/terms/{$guid}", [
  128. 'meaning' => '篡改',
  129. ], ['Authorization' => 'Bearer '.$modelToken])
  130. ->assertStatus(403);
  131. expect(DhammaTerm::find($guid)->meaning)->toBe('念处');
  132. });
  133. it('refuses a studio term written into someone elses studio', function () {
  134. $human = makeStudio('tester');
  135. $victim = makeStudio('victim');
  136. // 人类身份,但把 owner 指向别人的 studio
  137. $this->postJson('/api/v2/terms', [
  138. 'word' => 'satipaṭṭhāna',
  139. 'meaning' => '念处',
  140. 'studioName' => 'victim',
  141. 'language' => 'zh-Hans',
  142. ], authHeader($human))
  143. ->assertStatus(403);
  144. expect(DhammaTerm::count())->toBe(0);
  145. // 自己的 studio 则放行
  146. $this->postJson('/api/v2/terms', [
  147. 'word' => 'satipaṭṭhāna',
  148. 'meaning' => '念处',
  149. 'studioName' => 'tester',
  150. 'language' => 'zh-Hans',
  151. ], authHeader($human))->assertOk();
  152. expect(DhammaTerm::where('owner', $human)->count())->toBe(1);
  153. });