['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => ''], 'upstream_mirrors.connection' => 'business_mirror', 'cache.default' => 'array']); DB::purge('business_mirror'); Carbon::setTestNow('2026-10-20 12:00:00'); $schema = Schema::connection('business_mirror'); $schema->create(Differ::STATES, function (Blueprint $t): void { $t->string('dataset_key')->primary(); foreach (['status', 'source_key', 'source_object', 'target_table', 'run_mode', 'generation', 'last_error_code'] as $c) $t->string($c)->nullable(); foreach (['cursor', 'high_watermark'] as $c) $t->json($c)->nullable(); foreach (['window_started_at', 'window_ends_at', 'initial_completed_at', 'last_attempted_at', 'last_succeeded_at'] as $c) $t->dateTime($c, 6)->nullable(); $t->timestamps(); }); $schema->create('ops_jst_sku', function (Blueprint $t): void { $t->char('_mirror_key', 64)->primary(); $t->string('sku_type'); $t->string('sku_id'); $t->decimal('cost_price', 20, 4)->nullable(); $t->dateTime('modified')->nullable(); $t->dateTime('_mirror_synced_at', 6); }); $schema->create(Differ::SNAPSHOTS, function (Blueprint $t): void { $t->char('source_key', 64)->primary(); $t->string('sku_id'); $t->decimal('cost_price', 20, 4); $t->dateTime('captured_at', 6); }); $schema->create(Differ::OBSERVATIONS, function (Blueprint $t): void { $t->char('id', 64)->primary(); $t->char('source_key', 64); $t->string('sku_id'); $t->decimal('before_price', 20, 4); $t->decimal('after_price', 20, 4); $t->dateTime('previous_observed_at', 6); $t->dateTime('observed_at', 6); $t->dateTime('source_modified_at', 6)->nullable(); }); DB::connection('business_mirror')->table(Differ::STATES)->insert([ 'dataset_key' => ProductCatalogMirrorRepository::DATASET_KEY, 'status' => 'succeeded', 'initial_completed_at' => '2026-10-19 00:00:00', 'last_succeeded_at' => '2026-10-20 11:59:00', ]); $this->differ = new Differ(new GenericBusinessMirrorRepository(app('db')), new BusinessMirrorDatasetRegistry()); } protected function tearDown(): void { Carbon::setTestNow(); parent::tearDown(); } public function test_baseline_is_resumable_and_does_not_invent_history_then_observes_at_capture_time(): void { $db = DB::connection('business_mirror'); $this->product('a', 'SKU-A', 12); $this->product('b', 'SKU-B', 20); $this->assertSame('running', $this->differ->sync(1, 1)['status']); $db->table(Differ::STATES)->where('dataset_key', Differ::DATASET_KEY)->update(['status' => 'failed']); $this->assertSame('succeeded', $this->differ->sync(1, 3)['status']); $this->assertSame(2, $db->table(Differ::SNAPSHOTS)->count()); $this->assertSame(0, $db->table(Differ::OBSERVATIONS)->count()); Carbon::setTestNow('2026-10-20 12:10:00'); $db->table('ops_jst_sku')->where('_mirror_key', 'a')->update(['cost_price' => 15.1234, '_mirror_synced_at' => '2026-10-20 12:09:00']); $result = $this->differ->sync(10, 2); $this->assertSame(1, $result['changes']); $event = $db->table(Differ::OBSERVATIONS)->first(); $this->assertSame('2026-10-20 12:10:00.000000', $event->observed_at); $this->assertSame('2026-01-01 00:00:00', $event->source_modified_at); $this->assertEquals(12, $event->before_price); $this->assertEquals(15.1234, $event->after_price); $this->assertSame(0, $this->differ->sync(10, 2)['changes']); $this->assertSame(1, $db->table(Differ::OBSERVATIONS)->count()); $this->assertFalse(Schema::connection('business_mirror')->hasTable('jushuitan_cost_price_history_rows')); $this->assertFalse(Schema::connection('business_mirror')->hasTable('jushuitan_cost_price_history_sync_states')); $rows = [(object) ['_mirror_key' => 'a'], (object) ['_mirror_key' => 'b']]; (new JushuitanCostPriceObservationRepository(new GenericBusinessMirrorRepository(app('db'))))->attach($rows); $this->assertCount(1, $rows[0]->jushuitan_cost_price_observations); $this->assertCount(0, $rows[1]->jushuitan_cost_price_observations); $this->assertStringContainsString('不是真实改价时间', $rows[0]->jushuitan_cost_price_observations[0]['remark']); } public function test_new_products_null_prices_and_same_sku_on_different_source_rows_do_not_invent_changes(): void { $this->product('a', 'SAME', 12); $this->product('b', 'SAME', 25); $this->product('c', 'NULL', null); $this->differ->sync(); Carbon::setTestNow('2026-10-20 12:10:00'); $this->product('d', 'NEW', 99, '2026-10-20 12:09:00'); $this->assertSame(0, $this->differ->sync()['changes']); $this->assertSame(3, DB::connection('business_mirror')->table(Differ::SNAPSHOTS)->count()); DB::connection('business_mirror')->table(Differ::STATES)->where('dataset_key', ProductCatalogMirrorRepository::DATASET_KEY)->update(['status' => 'running']); $this->assertSame('source_not_ready', $this->differ->sync()['status']); } private function product(string $key, string $sku, ?float $price, string $at = '2026-10-20 11:50:00'): void { DB::connection('business_mirror')->table('ops_jst_sku')->insert([ '_mirror_key' => $key, 'sku_id' => $sku, 'sku_type' => 'normal', 'cost_price' => $price, 'modified' => '2026-01-01 00:00:00', '_mirror_synced_at' => $at, ]); } }