OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

View File

@@ -0,0 +1,281 @@
<?php
namespace Unit\app\Domain\Install;
use Illuminate\Database\ConnectionInterface;
use Leantime\Domain\Install\Repositories\Install;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use Unit\TestCase;
/**
* Regression tests for migration update_sql_30524 (#3686) — the load-bearing
* backfill that copies each goal's legacy zp_canvas_items.milestoneId into a
* `tracked_by` edge on zp_entity_relationship. A mis-backfill silently corrupts
* goal↔milestone links for every existing install (and it already had a
* near-miss: it shipped unregistered in $dbUpdates once), so its behavior is
* pinned here with a faked connection — no DB.
*
* Covers: correct edge direction, junk/non-numeric skipped, deleted /
* non-milestone tickets skipped, SAME-PROJECT enforcement (a legacy
* cross-project row is never promoted to an edge), NULL author for unknown,
* idempotent re-run (existing edge not duplicated), and the table-guard no-op.
*/
class UpdateSql30524Test extends TestCase
{
use MockeryPHPUnitIntegration;
/**
* Run update_sql_30524 against a fully-faked, TABLE-AWARE connection.
*
* @param array<int, object> $canvasGoals rows shaped {id, milestoneId, author, canvasId}
* @param array<int, int> $liveMilestones ticket id => projectId for live milestones
* @param array<int, int> $canvasProjects canvas id => projectId
* @param array<int, array{0:int,1:int}> $existingEdges [goalId, milestoneId] pairs already linked
* @param bool $tablesExist Schema-guard toggle: false makes hasTable/hasColumn report missing tables (the no-op path)
* @return array{result: mixed, inserted: array<int, array<string, mixed>>}
*/
private function runMigration(
array $canvasGoals,
array $liveMilestones,
array $canvasProjects,
array $existingEdges,
bool $tablesExist = true
): array {
$inserted = [];
$capture = function ($rows) use (&$inserted): void {
foreach ($rows as $row) {
$inserted[] = $row;
}
};
$schema = Mockery::mock();
$schema->shouldReceive('hasTable')->andReturn($tablesExist);
$schema->shouldReceive('hasColumn')->andReturn($tablesExist);
$conn = Mockery::mock(ConnectionInterface::class);
$conn->shouldReceive('getSchemaBuilder')->andReturn($schema);
$conn->shouldReceive('table')->andReturnUsing(
fn (string $table) => $this->fakeBuilder($table, $canvasGoals, $liveMilestones, $canvasProjects, $existingEdges, $capture)
);
$install = (new \ReflectionClass(Install::class))->newInstanceWithoutConstructor();
$prop = new \ReflectionProperty(Install::class, 'connection');
$prop->setAccessible(true);
$prop->setValue($install, $conn);
return ['result' => $install->update_sql_30524(), 'inserted' => $inserted];
}
/**
* The builder is table-aware: the migration reads goal rows (chunkById on
* zp_canvas_items), live milestones with their project (get on zp_tickets),
* goal projects via canvases (get on zp_canvas), and existing edges
* (get on zp_entity_relationship) — each table serves its own shape.
*/
private function fakeBuilder(
string $table,
array $canvasGoals,
array $liveMilestones,
array $canvasProjects,
array $existingEdges,
callable $capture
): object {
return new class($table, $canvasGoals, $liveMilestones, $canvasProjects, $existingEdges, $capture)
{
public function __construct(
private string $table,
private array $canvasGoals,
private array $liveMilestones,
private array $canvasProjects,
private array $existingEdges,
private $capture
) {}
public function where(...$a): static
{
return $this;
}
public function whereNotNull(...$a): static
{
return $this;
}
public function whereIn(...$a): static
{
return $this;
}
public function select(...$a): static
{
return $this;
}
public function orderBy(...$a): static
{
return $this;
}
public function chunkById($count, $callback): bool
{
$callback(collect($this->canvasGoals));
return true;
}
public function get($columns = ['*'])
{
if ($this->table === 'zp_tickets') {
return collect(array_map(
fn ($id, $projectId) => (object) ['id' => $id, 'projectId' => $projectId],
array_keys($this->liveMilestones),
array_values($this->liveMilestones)
));
}
if ($this->table === 'zp_canvas') {
return collect(array_map(
fn ($id, $projectId) => (object) ['id' => $id, 'projectId' => $projectId],
array_keys($this->canvasProjects),
array_values($this->canvasProjects)
));
}
// zp_entity_relationship — the existing-edge dedup read.
return collect(array_map(
fn ($e) => (object) ['entityA' => $e[0], 'entityB' => $e[1]],
$this->existingEdges
));
}
public function insert($rows): bool
{
($this->capture)($rows);
return true;
}
};
}
private function goal(int $id, ?string $milestoneId, ?int $author, int $canvasId = 1): object
{
return (object) ['id' => $id, 'milestoneId' => $milestoneId, 'author' => $author, 'canvasId' => $canvasId];
}
public function test_backfills_a_tracked_by_edge_in_the_correct_direction(): void
{
$out = $this->runMigration(
canvasGoals: [$this->goal(5, '42', 7)],
liveMilestones: [42 => 1],
canvasProjects: [1 => 1],
existingEdges: [],
);
$this->assertTrue($out['result']);
$this->assertCount(1, $out['inserted']);
$edge = $out['inserted'][0];
$this->assertSame(5, $edge['entityA']);
$this->assertSame('GoalItem', $edge['entityAType']);
$this->assertSame(42, $edge['entityB']);
$this->assertSame('Ticket', $edge['entityBType']);
$this->assertSame('tracked_by', $edge['relationship']);
$this->assertSame(7, $edge['createdBy']);
}
public function test_skips_junk_and_non_numeric_milestone_ids(): void
{
$out = $this->runMigration(
canvasGoals: [$this->goal(6, 'abc', 1), $this->goal(7, ' ', 1), $this->goal(8, '4x', 1)],
liveMilestones: [42 => 1],
canvasProjects: [1 => 1],
existingEdges: [],
);
$this->assertSame([], $out['inserted'], 'non-numeric milestoneId values are skipped');
}
public function test_skips_deleted_or_non_milestone_tickets(): void
{
// Goal points at ticket 99, which is not in the live-milestone set
// (deleted, or demoted to a task).
$out = $this->runMigration(
canvasGoals: [$this->goal(9, '99', 1)],
liveMilestones: [],
canvasProjects: [1 => 1],
existingEdges: [],
);
$this->assertSame([], $out['inserted'], 'a milestone that is not live is not backfilled');
}
public function test_skips_cross_project_legacy_rows(): void
{
// Product rule: goal↔milestone links are SAME-PROJECT only. A legacy
// column row pointing at another project's milestone must not be
// promoted into a first-class edge (goal's canvas 1 -> project 1;
// milestone 42 lives in project 2).
$out = $this->runMigration(
canvasGoals: [$this->goal(5, '42', 7, canvasId: 1)],
liveMilestones: [42 => 2],
canvasProjects: [1 => 1],
existingEdges: [],
);
$this->assertSame([], $out['inserted'], 'a cross-project legacy row is never promoted to an edge');
}
public function test_migrates_same_project_rows_alongside_skipped_cross_project_ones(): void
{
// Mixed chunk: goal 5's milestone is same-project (migrates), goal 6's
// is cross-project (skipped) — the guard is per-pair, not per-chunk.
$out = $this->runMigration(
canvasGoals: [$this->goal(5, '42', 7, canvasId: 1), $this->goal(6, '43', 7, canvasId: 1)],
liveMilestones: [42 => 1, 43 => 2],
canvasProjects: [1 => 1],
existingEdges: [],
);
$this->assertCount(1, $out['inserted']);
$this->assertSame(5, $out['inserted'][0]['entityA']);
$this->assertSame(42, $out['inserted'][0]['entityB']);
}
public function test_is_idempotent_when_the_edge_already_exists(): void
{
$out = $this->runMigration(
canvasGoals: [$this->goal(5, '42', 7)],
liveMilestones: [42 => 1],
canvasProjects: [1 => 1],
existingEdges: [[5, 42]],
);
$this->assertSame([], $out['inserted'], 'a re-run does not duplicate an existing edge');
}
public function test_unknown_author_is_stored_as_null_not_zero(): void
{
$out = $this->runMigration(
canvasGoals: [$this->goal(9, '42', null)],
liveMilestones: [42 => 1],
canvasProjects: [1 => 1],
existingEdges: [],
);
$this->assertCount(1, $out['inserted']);
$this->assertNull($out['inserted'][0]['createdBy'], 'unknown author stays NULL, never 0');
}
public function test_is_a_no_op_when_the_required_tables_are_absent(): void
{
$out = $this->runMigration(
canvasGoals: [$this->goal(5, '42', 7)],
liveMilestones: [42 => 1],
canvasProjects: [1 => 1],
existingEdges: [],
tablesExist: false,
);
$this->assertTrue($out['result']);
$this->assertSame([], $out['inserted'], 'missing schema short-circuits before any write');
}
}