chore: add tooling config, test bootstrap, env templates and docs

PHPUnit config (phpunit.dist.xml, bin/phpunit, bootstrap.php), PHP CS
Fixer config, .editorconfig. Separate .env.dev/.env.test templates.
Ollama tunnel setup script. Architecture and plan docs. Updated
application-layer unit tests to match current service signatures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simon Kuehn 2026-05-17 22:44:16 +00:00
parent 2cfc5e8f17
commit f55e96b094
21 changed files with 12348 additions and 25 deletions

17
.editorconfig Normal file
View file

@ -0,0 +1,17 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[{compose.yaml,compose.*.yaml}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false

4
.env.dev Normal file
View file

@ -0,0 +1,4 @@
###> symfony/framework-bundle ###
APP_SECRET=7ac10a47af9d9582c01fe117c77e4c53
###< symfony/framework-bundle ###

3
.env.test Normal file
View file

@ -0,0 +1,3 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'

17
.php-cs-fixer.dist.php Normal file
View file

@ -0,0 +1,17 @@
<?php
$finder = (new PhpCsFixer\Finder())
->in(__DIR__)
->exclude('var')
->notPath([
'config/bundles.php',
'config/reference.php',
])
;
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
])
->setFinder($finder)
;

4
bin/phpunit Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env php
<?php
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';

View file

@ -0,0 +1,94 @@
#!/usr/bin/env bash
# Run this script on the LOCAL machine (where Ollama runs).
# It registers your SSH public key on the server and installs
# the autossh systemd service for a persistent tunnel.
#
# Usage: ./setup.sh <server-ip-or-hostname>
#
# Prerequisites (local machine):
# apt/brew: openssh-client autossh
# Ollama running on localhost:11434
set -euo pipefail
SERVER="${1:?Usage: $0 <server-ip-or-hostname>}"
TUNNEL_USER="ollama-tunnel"
REMOTE_PORT=11434
LOCAL_PORT=11434
KEY_FILE="${HOME}/.ssh/id_ed25519"
# Generate key if it doesn't exist
if [[ ! -f "${KEY_FILE}" ]]; then
echo "[+] Generating SSH key ${KEY_FILE} ..."
ssh-keygen -t ed25519 -f "${KEY_FILE}" -N "" -C "ollama-tunnel@$(hostname)"
fi
# Copy public key to server
echo "[+] Copying public key to ${TUNNEL_USER}@${SERVER} ..."
echo " You will be prompted for sudo on the server (or use the superseller account)."
PUBKEY=$(cat "${KEY_FILE}.pub")
ssh superseller@"${SERVER}" "sudo bash -c 'echo \"${PUBKEY}\" >> /home/${TUNNEL_USER}/.ssh/authorized_keys && sort -u /home/${TUNNEL_USER}/.ssh/authorized_keys -o /home/${TUNNEL_USER}/.ssh/authorized_keys'"
echo "[+] Testing tunnel connection ..."
ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 \
-N -i "${KEY_FILE}" \
-R "172.18.0.1:${REMOTE_PORT}:localhost:${LOCAL_PORT}" \
"${TUNNEL_USER}@${SERVER}" &
SSH_PID=$!
sleep 2
if kill -0 "${SSH_PID}" 2>/dev/null; then
echo "[+] Tunnel works! Stopping test connection."
kill "${SSH_PID}"
else
echo "[!] Tunnel test failed. Check sshd config and firewall on the server."
exit 1
fi
# Install systemd service
install_systemd_service() {
local service_file="${HOME}/.config/systemd/user/ollama-tunnel.service"
mkdir -p "$(dirname "${service_file}")"
cat > "${service_file}" << EOF
[Unit]
Description=Ollama SSH reverse tunnel to SuperSeller3000 server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/autossh -M 0 \\
-o "ServerAliveInterval=30" \\
-o "ServerAliveCountMax=3" \\
-o "ExitOnForwardFailure=yes" \\
-o "StrictHostKeyChecking=accept-new" \\
-N -i ${KEY_FILE} \\
-R 172.18.0.1:${REMOTE_PORT}:localhost:${LOCAL_PORT} \\
${TUNNEL_USER}@${SERVER}
Restart=always
RestartSec=10
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user enable ollama-tunnel.service
systemctl --user start ollama-tunnel.service
echo "[+] systemd service installed and started."
echo " Status: systemctl --user status ollama-tunnel"
}
if command -v autossh &>/dev/null && command -v systemctl &>/dev/null; then
echo "[+] Installing autossh systemd user service ..."
install_systemd_service
else
echo "[!] autossh or systemd not found. Manual tunnel command:"
echo ""
echo " autossh -M 0 -o ServerAliveInterval=30 -N \\"
echo " -i ${KEY_FILE} \\"
echo " -R 172.18.0.1:${REMOTE_PORT}:localhost:${LOCAL_PORT} \\"
echo " ${TUNNEL_USER}@${SERVER}"
fi
echo ""
echo "Done. The server will see Ollama at http://172.18.0.1:${REMOTE_PORT}"

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

44
phpunit.dist.xml Normal file
View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
failOnDeprecation="true"
failOnNotice="true"
failOnWarning="true"
bootstrap="tests/bootstrap.php"
cacheDirectory=".phpunit.cache"
>
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<source ignoreSuppressionOfDeprecations="true"
ignoreIndirectDeprecations="true"
restrictNotices="true"
restrictWarnings="true"
>
<include>
<directory>src</directory>
</include>
<deprecationTrigger>
<method>Doctrine\Deprecations\Deprecation::trigger</method>
<method>Doctrine\Deprecations\Deprecation::delegateTriggerToBackend</method>
<function>trigger_deprecation</function>
</deprecationTrigger>
</source>
<extensions>
</extensions>
</phpunit>

View file

@ -23,7 +23,7 @@ final class ArticleTypeServiceTest extends TestCase
$this->service = new ArticleTypeService($this->repo);
}
public function test_create_saves_article_type(): void
public function testCreateSavesArticleType(): void
{
$this->repo->expects($this->once())->method('save');
@ -32,7 +32,7 @@ final class ArticleTypeServiceTest extends TestCase
$this->assertSame('Notebook', $type->getName());
}
public function test_rename_updates_name(): void
public function testRenameUpdatesName(): void
{
$type = new ArticleType('Notebook');
$this->repo->method('findById')->willReturn($type);
@ -43,7 +43,7 @@ final class ArticleTypeServiceTest extends TestCase
$this->assertSame('Laptop', $type->getName());
}
public function test_rename_throws_when_not_found(): void
public function testRenameThrowsWhenNotFound(): void
{
$this->repo->method('findById')->willReturn(null);
@ -52,7 +52,7 @@ final class ArticleTypeServiceTest extends TestCase
$this->service->rename(\Symfony\Component\Uid\Uuid::v7(), 'X');
}
public function test_add_attribute_links_definition(): void
public function testAddAttributeLinksDefinition(): void
{
$type = new ArticleType('Notebook');
$def = new AttributeDefinition('RAM', AttributeType::String);

View file

@ -30,7 +30,7 @@ final class ArticleValidatorTest extends TestCase
$this->type->addAttributeDefinition($this->cpuDef);
}
public function test_valid_when_all_attributes_set(): void
public function testValidWhenAllAttributesSet(): void
{
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);
$article->setAttributeValue(new AttributeValue($article, $this->ramDef, '16 GB'));
@ -42,7 +42,7 @@ final class ArticleValidatorTest extends TestCase
$this->assertTrue($this->validator->isValid($article));
}
public function test_returns_missing_attribute_names(): void
public function testReturnsMissingAttributeNames(): void
{
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);
$article->setAttributeValue(new AttributeValue($article, $this->ramDef, '16 GB'));
@ -55,7 +55,7 @@ final class ArticleValidatorTest extends TestCase
$this->assertFalse($this->validator->isValid($article));
}
public function test_all_missing_when_no_values_set(): void
public function testAllMissingWhenNoValuesSet(): void
{
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);

View file

@ -31,7 +31,7 @@ final class LocalStorageManagerTest extends TestCase
}
}
public function test_store_picks_active_path_with_quota(): void
public function testStorePicksActivePathWithQuota(): void
{
$path = new StoragePath('Main', sys_get_temp_dir().'/storage-test-'.uniqid(), 1_000_000, 10);
mkdir($path->getBasePath(), recursive: true);
@ -51,7 +51,7 @@ final class LocalStorageManagerTest extends TestCase
rmdir($path->getBasePath());
}
public function test_throws_when_no_active_path(): void
public function testThrowsWhenNoActivePath(): void
{
$this->repo->method('findActiveSortedByPriority')->willReturn([]);
@ -61,7 +61,7 @@ final class LocalStorageManagerTest extends TestCase
$this->manager->store($this->tmpFile, 'photo.jpg');
}
public function test_skips_full_path_and_uses_next(): void
public function testSkipsFullPathAndUsesNext(): void
{
$fullPath = new StoragePath('Full', sys_get_temp_dir().'/full-'.uniqid(), 50, 20);
$okPath = new StoragePath('OK', sys_get_temp_dir().'/ok-'.uniqid(), 1_000_000, 10);
@ -80,7 +80,7 @@ final class LocalStorageManagerTest extends TestCase
rmdir($okPath->getBasePath());
}
public function test_get_full_path(): void
public function testGetFullPath(): void
{
$path = new StoragePath('Main', '/srv/storage', 1_000_000, 10);
$this->assertSame('/srv/storage/photo.jpg', $this->manager->getFullPath($path, 'photo.jpg'));

View file

@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase;
final class ArticleStatusTest extends TestCase
{
public function test_valid_transitions(): void
public function testValidTransitions(): void
{
$this->assertTrue(ArticleStatus::Ingesting->canTransitionTo(ArticleStatus::Draft));
$this->assertTrue(ArticleStatus::Draft->canTransitionTo(ArticleStatus::Active));
@ -19,7 +19,7 @@ final class ArticleStatusTest extends TestCase
$this->assertTrue(ArticleStatus::Listed->canTransitionTo(ArticleStatus::Sold));
}
public function test_invalid_transitions(): void
public function testInvalidTransitions(): void
{
$this->assertFalse(ArticleStatus::Sold->canTransitionTo(ArticleStatus::Draft));
$this->assertFalse(ArticleStatus::Ingesting->canTransitionTo(ArticleStatus::Sold));

View file

@ -19,7 +19,7 @@ final class ArticleTest extends TestCase
$this->type = new ArticleType('Notebook');
}
public function test_new_article_has_ingesting_status(): void
public function testNewArticleHasIngestingStatus(): void
{
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);
@ -27,7 +27,7 @@ final class ArticleTest extends TestCase
$this->assertSame(1, $article->getStock());
}
public function test_valid_status_transition(): void
public function testValidStatusTransition(): void
{
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);
$article->transitionTo(ArticleStatus::Draft);
@ -35,7 +35,7 @@ final class ArticleTest extends TestCase
$this->assertSame(ArticleStatus::Draft, $article->getStatus());
}
public function test_invalid_status_transition_throws(): void
public function testInvalidStatusTransitionThrows(): void
{
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);
@ -43,7 +43,7 @@ final class ArticleTest extends TestCase
$article->transitionTo(ArticleStatus::Sold);
}
public function test_decrement_stock(): void
public function testDecrementStock(): void
{
$article = new Article($this->type, 'NB-001', 'INV-001', 3, ArticleCondition::Good);
$article->decrementStock();
@ -52,7 +52,7 @@ final class ArticleTest extends TestCase
$this->assertFalse($article->isOutOfStock());
}
public function test_decrement_to_zero_marks_out_of_stock(): void
public function testDecrementToZeroMarksOutOfStock(): void
{
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);
$article->decrementStock();
@ -60,7 +60,7 @@ final class ArticleTest extends TestCase
$this->assertTrue($article->isOutOfStock());
}
public function test_decrement_below_zero_throws(): void
public function testDecrementBelowZeroThrows(): void
{
$article = new Article($this->type, 'NB-001', 'INV-001', 0, ArticleCondition::Good);

View file

@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase;
final class CustomerTest extends TestCase
{
public function test_new_customer_has_empty_platform_ids(): void
public function testNewCustomerHasEmptyPlatformIds(): void
{
$customer = new Customer('Max Mustermann', 'max@example.com', []);
@ -17,7 +17,7 @@ final class CustomerTest extends TestCase
$this->assertNull($customer->getPlatformId('ebay'));
}
public function test_add_platform_id(): void
public function testAddPlatformId(): void
{
$customer = new Customer('Max Mustermann', 'max@example.com', []);
$customer->addPlatformId('ebay', 'ebay-user-123');
@ -25,7 +25,7 @@ final class CustomerTest extends TestCase
$this->assertSame('ebay-user-123', $customer->getPlatformId('ebay'));
}
public function test_matching_key_is_lowercase_normalized(): void
public function testMatchingKeyIsLowercaseNormalized(): void
{
$customer = new Customer('Max Mustermann', 'max@example.com', [
'street' => 'Musterstraße 1',

View file

@ -1,12 +1,12 @@
<?php
declare(strict_types=1);
use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__).'/vendor/autoload.php';
if (method_exists(Dotenv::class, 'bootEnv')) {
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
}
if ($_SERVER['APP_DEBUG']) {
umask(0000);

0
translations/.gitignore vendored Normal file
View file