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,168 @@
<?php
namespace Unit\app\Domain\ContentTemplates\Services\Appliers;
use Leantime\Core\Db\Db as DbCore;
use Leantime\Domain\ContentTemplates\Models\ContentTemplate;
use Leantime\Domain\ContentTemplates\Services\Appliers\CanvasItemsApplier;
use Leantime\Domain\ContentTemplates\Services\Appliers\WikiApplier;
use Leantime\Domain\ContentTemplates\Services\ContentTemplateRegistry;
use Unit\TestCase;
/**
* Unit tests for the appliers' supports() routing logic and early-return
* safety. Actual DB writes are exercised in integration tests once Phase 2
* wires real templates; here we lock in the routing contract.
*/
class AppliersTest extends TestCase
{
use \Codeception\Test\Feature\Stub;
public function test_wiki_applier_supports_only_wiki(): void
{
$applier = new WikiApplier($this->makeDbCore());
$this->assertTrue($applier->supports('wiki'));
$this->assertFalse($applier->supports('logicmodel'));
$this->assertFalse($applier->supports('goal'));
$this->assertFalse($applier->supports(''));
}
public function test_canvas_applier_supports_any_non_wiki_non_empty_applies_to(): void
{
$applier = new CanvasItemsApplier($this->makeDbCore());
$this->assertTrue($applier->supports('logicmodel'));
$this->assertTrue($applier->supports('goal'));
$this->assertTrue($applier->supports('leancanvas'));
$this->assertTrue($applier->supports('swot'));
$this->assertTrue($applier->supports('any-future-canvas-type'));
$this->assertFalse($applier->supports('wiki'));
$this->assertFalse($applier->supports(''));
}
public function test_canvas_applier_returns_zero_for_invalid_target_id(): void
{
$applier = new CanvasItemsApplier($this->makeDbCore());
$this->assertSame(0, $applier->apply(0, $this->makeCanvasTemplate()));
$this->assertSame(0, $applier->apply(-1, $this->makeCanvasTemplate()));
}
public function test_canvas_applier_returns_zero_for_unusable_template(): void
{
$applier = new CanvasItemsApplier($this->makeDbCore());
$unusable = ContentTemplate::fromArray([
'key' => '',
'title' => 'X',
'description' => '',
'appliesTo' => 'logicmodel',
]);
$this->assertSame(0, $applier->apply(42, $unusable));
}
public function test_canvas_applier_returns_zero_for_empty_items_payload(): void
{
$applier = new CanvasItemsApplier($this->makeDbCore());
$emptyItems = ContentTemplate::fromArray([
'key' => 'empty',
'title' => 'Empty',
'description' => '',
'appliesTo' => 'logicmodel',
'logicmodel' => ['items' => []],
]);
$this->assertSame(0, $applier->apply(42, $emptyItems));
}
public function test_wiki_applier_returns_zero_for_invalid_target_id(): void
{
$applier = new WikiApplier($this->makeDbCore());
$this->assertSame(0, $applier->apply(0, $this->makeWikiTemplate()));
}
public function test_wiki_applier_returns_zero_for_empty_articles_payload(): void
{
$applier = new WikiApplier($this->makeDbCore());
$emptyArticles = ContentTemplate::fromArray([
'key' => 'empty',
'title' => 'Empty',
'description' => '',
'appliesTo' => 'wiki',
'wiki' => ['articles' => []],
]);
$this->assertSame(0, $applier->apply(42, $emptyArticles));
}
public function test_registry_applier_for_falls_back_to_supports_when_no_explicit_binding(): void
{
$registry = new ContentTemplateRegistry;
$canvas = new CanvasItemsApplier($this->makeDbCore());
$wiki = new WikiApplier($this->makeDbCore());
// Bind WikiApplier on 'wiki' and CanvasItemsApplier on 'logicmodel'.
// Ask the registry for 'cp' (a canvas type that nobody explicitly
// bound). The fallback should find CanvasItemsApplier via supports().
$registry->registerApplier('wiki', $wiki);
$registry->registerApplier('logicmodel', $canvas);
$this->assertSame($canvas, $registry->applierFor('cp'));
$this->assertSame($canvas, $registry->applierFor('swot'));
$this->assertSame($wiki, $registry->applierFor('wiki'));
$this->assertSame($canvas, $registry->applierFor('logicmodel'));
}
public function test_registry_applier_for_returns_null_when_no_applier_supports_type(): void
{
$registry = new ContentTemplateRegistry;
$registry->registerApplier('wiki', new WikiApplier($this->makeDbCore()));
// 'logicmodel' isn't bound and WikiApplier doesn't support it.
$this->assertNull($registry->applierFor('logicmodel'));
}
private function makeDbCore(): DbCore
{
// The supports() / early-return paths never call the connection, so a
// bare stub is enough. Methods that DO write are exercised in
// integration tests (Phase 2+).
return $this->make(DbCore::class);
}
private function makeCanvasTemplate(): ContentTemplate
{
return ContentTemplate::fromArray([
'key' => 'k',
'title' => 'T',
'description' => '',
'appliesTo' => 'logicmodel',
'logicmodel' => [
'items' => [
['box' => 'lm_inputs', 'title' => 'A', 'description' => 'aa'],
],
],
]);
}
private function makeWikiTemplate(): ContentTemplate
{
return ContentTemplate::fromArray([
'key' => 'k',
'title' => 'T',
'description' => '',
'appliesTo' => 'wiki',
'wiki' => [
'articles' => [
['title' => 'A', 'content' => '<p>aa</p>'],
],
],
]);
}
}

View File

@@ -0,0 +1,197 @@
<?php
namespace Unit\app\Domain\ContentTemplates\Services;
use Leantime\Domain\ContentTemplates\Models\ContentTemplate;
use Leantime\Domain\ContentTemplates\Services\ContentTemplateRegistry;
use Unit\TestCase;
/**
* Unit tests for ContentTemplateRegistry.
*
* Sets up a tmp library root containing one logicmodel template and one wiki
* template, then exercises load / forAppliesTo / get / overrides.
*/
class ContentTemplateRegistryTest extends TestCase
{
private string $tmpRoot;
protected function setUp(): void
{
parent::setUp();
$this->tmpRoot = sys_get_temp_dir().'/ct-registry-test-'.uniqid();
mkdir($this->tmpRoot.'/logicmodel', 0o777, true);
mkdir($this->tmpRoot.'/wiki', 0o777, true);
file_put_contents($this->tmpRoot.'/logicmodel/sample.yaml', <<<'YAML'
key: "sample"
title: "Sample LM"
description: "Test fixture."
appliesTo: "logicmodel"
sector: "test"
logicmodel:
items:
- box: "lm_inputs"
title: "Item"
description: "Desc"
YAML);
file_put_contents($this->tmpRoot.'/wiki/notes.yaml', <<<'YAML'
key: "notes"
title: "Notes"
description: "Wiki test."
appliesTo: "wiki"
wiki:
articles:
- title: "Hello"
content: "<p>Hi</p>"
YAML);
}
protected function tearDown(): void
{
$this->rmrf($this->tmpRoot);
parent::tearDown();
}
public function test_for_applies_to_returns_templates_under_that_bucket(): void
{
$registry = new ContentTemplateRegistry;
$registry->registerLibraryRoot($this->tmpRoot);
$lm = $registry->forAppliesTo('logicmodel');
$wiki = $registry->forAppliesTo('wiki');
// The registry constructor auto-registers the core library root, so
// built-in templates show up alongside the tmp ones. Pin contract on
// "tmp fixtures are present and well-shaped" rather than exact count.
$this->assertArrayHasKey('sample', $lm);
$this->assertInstanceOf(ContentTemplate::class, $lm['sample']);
$this->assertSame('logicmodel', $lm['sample']->appliesTo);
$this->assertArrayHasKey('notes', $wiki);
$this->assertSame('wiki', $wiki['notes']->appliesTo);
}
public function test_get_returns_single_template_by_applies_to_and_key(): void
{
$registry = new ContentTemplateRegistry;
$registry->registerLibraryRoot($this->tmpRoot);
$tpl = $registry->get('logicmodel', 'sample');
$this->assertNotNull($tpl);
$this->assertSame('Sample LM', $tpl->title);
$this->assertSame('test', $tpl->sector);
}
public function test_get_returns_null_for_unknown_template(): void
{
$registry = new ContentTemplateRegistry;
$registry->registerLibraryRoot($this->tmpRoot);
$this->assertNull($registry->get('logicmodel', 'does-not-exist'));
$this->assertNull($registry->get('unknown-applies-to', 'sample'));
}
public function test_directory_name_overrides_applies_to_in_yaml(): void
{
// YAML claims appliesTo=wiki but the file is in the logicmodel
// directory. The registry should rewrite the appliesTo to match the
// directory, so a typo in the YAML can't pollute the wrong bucket.
file_put_contents($this->tmpRoot.'/logicmodel/lies.yaml', <<<'YAML'
key: "lies"
title: "Liar"
description: "Wrong appliesTo claim."
appliesTo: "wiki"
wiki:
articles:
- title: "Bait"
content: ""
logicmodel:
items:
- box: "lm_inputs"
title: "Item"
YAML);
$registry = new ContentTemplateRegistry;
$registry->registerLibraryRoot($this->tmpRoot);
$byLm = $registry->get('logicmodel', 'lies');
$this->assertNotNull($byLm);
$this->assertSame('logicmodel', $byLm->appliesTo);
$this->assertNull($registry->get('wiki', 'lies'));
}
public function test_later_library_root_overrides_earlier_on_collision(): void
{
$secondRoot = sys_get_temp_dir().'/ct-registry-test-second-'.uniqid();
mkdir($secondRoot.'/logicmodel', 0o777, true);
file_put_contents($secondRoot.'/logicmodel/sample.yaml', <<<'YAML'
key: "sample"
title: "Override Title"
description: "From second root."
appliesTo: "logicmodel"
logicmodel:
items:
- box: "lm_outputs"
title: "Override Item"
YAML);
try {
$registry = new ContentTemplateRegistry;
$registry->registerLibraryRoot($this->tmpRoot);
$registry->registerLibraryRoot($secondRoot);
$tpl = $registry->get('logicmodel', 'sample');
$this->assertNotNull($tpl);
$this->assertSame('Override Title', $tpl->title);
} finally {
$this->rmrf($secondRoot);
}
}
public function test_register_library_root_is_idempotent(): void
{
$registry = new ContentTemplateRegistry;
$registry->registerLibraryRoot($this->tmpRoot);
$registry->registerLibraryRoot($this->tmpRoot);
$registry->registerLibraryRoot($this->tmpRoot.'/');
$lm = $registry->forAppliesTo('logicmodel');
$this->assertCount(1, $lm);
}
public function test_invalid_yaml_is_skipped_without_crashing(): void
{
file_put_contents($this->tmpRoot.'/logicmodel/broken.yaml', "key: [unterminated\n");
$registry = new ContentTemplateRegistry;
$registry->registerLibraryRoot($this->tmpRoot);
$lm = $registry->forAppliesTo('logicmodel');
$this->assertCount(1, $lm);
$this->assertArrayHasKey('sample', $lm);
$this->assertArrayNotHasKey('broken', $lm);
}
private function rmrf(string $dir): void
{
if (! is_dir($dir)) {
return;
}
foreach (scandir($dir) as $f) {
if ($f === '.' || $f === '..') {
continue;
}
$path = $dir.'/'.$f;
is_dir($path) ? $this->rmrf($path) : unlink($path);
}
rmdir($dir);
}
}