| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- <?php
- use App\Models\AiModel;
- use App\Models\DhammaTerm;
- use Illuminate\Foundation\Testing\RefreshDatabase;
- uses(RefreshDatabase::class);
- /**
- * 术语的写入链路与句子一致:人类签出 channel 的 access token,AI 模型带着
- * 它以自己的身份建/改术语。这里的核心同样是署名——editor_uid 必须是模型。
- */
- /** 人类签出 channel access token,并取出模型身份 token。 */
- function termWriteTokens($test, string $human, string $channel, AiModel $model): array
- {
- $accessToken = $test->postJson('/api/v2/access-token', [
- 'payload' => [[
- 'res_type' => 'channel',
- 'res_id' => $channel,
- 'power' => 'edit',
- 'book' => 0,
- ]],
- ], authHeader($human))
- ->assertOk()
- ->assertJsonPath('data.count', 1)
- ->json('data.rows.0.token');
- $modelToken = $test->getJson("/api/v2/ai-model-token/{$model->uid}", authHeader($human))
- ->assertOk()
- ->json('data.token');
- return [$accessToken, $modelToken];
- }
- it('creates a channel term attributed to the ai model', function () {
- $human = makeStudio('tester');
- $channel = makeChannel($human);
- $model = AiModel::factory()->ownedBy($human)->create(['name' => 'claude-opus-5']);
- [$accessToken, $modelToken] = termWriteTokens($this, $human, $channel, $model);
- $this->postJson('/api/v2/terms', [
- 'word' => 'satipaṭṭhāna',
- 'meaning' => '念处',
- 'other_meaning' => '念住',
- 'note' => '四念处之念处。',
- 'tag' => 'abhidhamma',
- 'channel' => $channel,
- 'access_token' => $accessToken,
- ], ['Authorization' => 'Bearer '.$modelToken])
- ->assertOk()
- ->assertJsonPath('data.word', 'satipaṭṭhāna');
- $saved = DhammaTerm::where('word', 'satipaṭṭhāna')->first();
- expect($saved)->not->toBeNull();
- expect($saved->meaning)->toBe('念处');
- expect($saved->channal)->toBe($channel);
- // 核心断言:署名归模型,而不是发起操作的人类
- expect($saved->editor_uid)->toBe($model->uid);
- expect($saved->editor_uid)->not->toBe($human);
- // editor_id 存人类的自增 sn,模型没有;0 与「缺省/未知」撞车,故记 -1
- expect($saved->editor_id)->toBe(-1);
- // owner 仍是 channel 所属 studio
- expect($saved->owner)->toBe($human);
- });
- it('refuses to create a term without an access token', function () {
- $human = makeStudio('tester');
- $channel = makeChannel($human);
- $model = AiModel::factory()->ownedBy($human)->create();
- $modelToken = $this->getJson("/api/v2/ai-model-token/{$model->uid}", authHeader($human))
- ->json('data.token');
- $this->postJson('/api/v2/terms', [
- 'word' => 'satipaṭṭhāna',
- 'meaning' => '念处',
- 'channel' => $channel,
- ], ['Authorization' => 'Bearer '.$modelToken])
- ->assertStatus(403);
- expect(DhammaTerm::count())->toBe(0);
- });
- it('refuses an access token issued for a different channel', function () {
- $human = makeStudio('tester');
- $channel = makeChannel($human, 'mine');
- $otherChannel = makeChannel(makeStudio('someone-else'), 'not mine');
- $model = AiModel::factory()->ownedBy($human)->create();
- [$accessToken, $modelToken] = termWriteTokens($this, $human, $channel, $model);
- $this->postJson('/api/v2/terms', [
- 'word' => 'satipaṭṭhāna',
- 'meaning' => '念处',
- 'channel' => $otherChannel,
- 'access_token' => $accessToken,
- ], ['Authorization' => 'Bearer '.$modelToken])
- ->assertStatus(403);
- expect(DhammaTerm::count())->toBe(0);
- });
- it('updates a term as the ai model, leaving unsubmitted fields untouched', function () {
- $human = makeStudio('tester');
- $channel = makeChannel($human);
- $model = AiModel::factory()->ownedBy($human)->create();
- [$accessToken, $modelToken] = termWriteTokens($this, $human, $channel, $model);
- $guid = $this->postJson('/api/v2/terms', [
- 'word' => 'satipaṭṭhāna',
- 'meaning' => '念处',
- 'note' => '原有的注解',
- 'tag' => 'abhidhamma',
- 'channel' => $channel,
- 'access_token' => $accessToken,
- ], ['Authorization' => 'Bearer '.$modelToken])
- ->assertOk()
- ->json('data.guid');
- $createTime = DhammaTerm::find($guid)->create_time;
- // 只提交 meaning:note/tag 必须原样保留
- $this->putJson("/api/v2/terms/{$guid}", [
- 'meaning' => '念住',
- 'access_token' => $accessToken,
- ], ['Authorization' => 'Bearer '.$modelToken])
- ->assertOk()
- ->assertJsonPath('data.meaning', '念住');
- $saved = DhammaTerm::find($guid);
- expect($saved->meaning)->toBe('念住');
- expect($saved->word)->toBe('satipaṭṭhāna');
- expect($saved->note)->toBe('原有的注解');
- expect($saved->tag)->toBe('abhidhamma');
- expect($saved->editor_uid)->toBe($model->uid);
- expect($saved->editor_id)->toBe(-1);
- // create_time 是创建时刻,改动不该刷新它
- expect($saved->create_time)->toBe($createTime);
- });
- it('refuses to update a term with no access token', function () {
- $human = makeStudio('tester');
- $channel = makeChannel($human);
- $model = AiModel::factory()->ownedBy($human)->create();
- [$accessToken, $modelToken] = termWriteTokens($this, $human, $channel, $model);
- $guid = $this->postJson('/api/v2/terms', [
- 'word' => 'satipaṭṭhāna',
- 'meaning' => '念处',
- 'channel' => $channel,
- 'access_token' => $accessToken,
- ], ['Authorization' => 'Bearer '.$modelToken])->json('data.guid');
- $this->putJson("/api/v2/terms/{$guid}", [
- 'meaning' => '篡改',
- ], ['Authorization' => 'Bearer '.$modelToken])
- ->assertStatus(403);
- expect(DhammaTerm::find($guid)->meaning)->toBe('念处');
- });
- it('refuses a studio term written into someone elses studio', function () {
- $human = makeStudio('tester');
- $victim = makeStudio('victim');
- // 人类身份,但把 owner 指向别人的 studio
- $this->postJson('/api/v2/terms', [
- 'word' => 'satipaṭṭhāna',
- 'meaning' => '念处',
- 'studioName' => 'victim',
- 'language' => 'zh-Hans',
- ], authHeader($human))
- ->assertStatus(403);
- expect(DhammaTerm::count())->toBe(0);
- // 自己的 studio 则放行
- $this->postJson('/api/v2/terms', [
- 'word' => 'satipaṭṭhāna',
- 'meaning' => '念处',
- 'studioName' => 'tester',
- 'language' => 'zh-Hans',
- ], authHeader($human))->assertOk();
- expect(DhammaTerm::where('owner', $human)->count())->toBe(1);
- // 人类写入不受影响:editor_id 仍是 token 里的用户 sn
- expect(DhammaTerm::where('owner', $human)->first()->editor_id)->toBe(1);
- });
- it('refuses to create a studio-level term as an ai model', function () {
- $human = makeStudio('tester');
- $model = AiModel::factory()->ownedBy($human)->create();
- $modelToken = $this->getJson("/api/v2/ai-model-token/{$model->uid}", authHeader($human))
- ->json('data.token');
- // 不给 channel:模型没有任何可核验的授权,必须被拒
- $this->postJson('/api/v2/terms', [
- 'word' => 'satipaṭṭhāna',
- 'meaning' => '念处',
- 'studioName' => 'tester',
- 'language' => 'zh-Hans',
- ], ['Authorization' => 'Bearer '.$modelToken])
- ->assertStatus(403);
- expect(DhammaTerm::count())->toBe(0);
- });
- it('refuses to edit a studio-level term as an ai model', function () {
- $human = makeStudio('tester');
- $model = AiModel::factory()->ownedBy($human)->create();
- // 人类先建一条 studio 级术语
- $guid = $this->postJson('/api/v2/terms', [
- 'word' => 'satipaṭṭhāna',
- 'meaning' => '念处',
- 'studioName' => 'tester',
- 'language' => 'zh-Hans',
- ], authHeader($human))->assertOk()->json('data.guid');
- $modelToken = $this->getJson("/api/v2/ai-model-token/{$model->uid}", authHeader($human))
- ->json('data.token');
- $this->putJson("/api/v2/terms/{$guid}", [
- 'meaning' => '篡改',
- ], ['Authorization' => 'Bearer '.$modelToken])
- ->assertStatus(403);
- expect(DhammaTerm::find($guid)->meaning)->toBe('念处');
- });
- it('rejects a duplicate word+tag in the same channel written by an ai model', function () {
- $human = makeStudio('tester');
- $channel = makeChannel($human);
- $model = AiModel::factory()->ownedBy($human)->create();
- [$accessToken, $modelToken] = termWriteTokens($this, $human, $channel, $model);
- $payload = [
- 'word' => 'satipaṭṭhāna',
- 'meaning' => '念处',
- 'tag' => 'abhidhamma',
- 'channel' => $channel,
- 'access_token' => $accessToken,
- ];
- $this->postJson('/api/v2/terms', $payload, ['Authorization' => 'Bearer '.$modelToken])
- ->assertOk();
- // 查重按 channel,不按 owner——模型的 uid 与落库的 owner 不等,
- // 按 owner 查会永远查不到重复,同一个词被反复插入。
- $this->postJson('/api/v2/terms', $payload, ['Authorization' => 'Bearer '.$modelToken])
- ->assertOk()
- ->assertJsonPath('message', 'word existed');
- expect(DhammaTerm::where('word', 'satipaṭṭhāna')->count())->toBe(1);
- });
- it('exposes the channel of a channel term', function () {
- $human = makeStudio('tester');
- $channel = makeChannel($human, 'my channel');
- $model = AiModel::factory()->ownedBy($human)->create();
- [$accessToken, $modelToken] = termWriteTokens($this, $human, $channel, $model);
- $guid = $this->postJson('/api/v2/terms', [
- 'word' => 'satipaṭṭhāna',
- 'meaning' => '念处',
- 'channel' => $channel,
- 'access_token' => $accessToken,
- ], ['Authorization' => 'Bearer '.$modelToken])->json('data.guid');
- // channel 信息取自 channal 列;此前读的是模型上并不存在的 channel_id
- $this->getJson("/api/v2/terms/{$guid}", authHeader($human))
- ->assertOk()
- ->assertJsonPath('data.channel.name', 'my channel');
- });
|