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:
parent
2cfc5e8f17
commit
f55e96b094
21 changed files with 12348 additions and 25 deletions
17
.editorconfig
Normal file
17
.editorconfig
Normal 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
4
.env.dev
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_SECRET=7ac10a47af9d9582c01fe117c77e4c53
|
||||
###< symfony/framework-bundle ###
|
||||
3
.env.test
Normal file
3
.env.test
Normal 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
17
.php-cs-fixer.dist.php
Normal 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
4
bin/phpunit
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||
94
docker/ollama-tunnel/setup.sh
Normal file
94
docker/ollama-tunnel/setup.sh
Normal 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}"
|
||||
3094
docs/superpowers/plans/2026-05-13-01-foundation.md
Normal file
3094
docs/superpowers/plans/2026-05-13-01-foundation.md
Normal file
File diff suppressed because it is too large
Load diff
2461
docs/superpowers/plans/2026-05-13-02-article-api.md
Normal file
2461
docs/superpowers/plans/2026-05-13-02-article-api.md
Normal file
File diff suppressed because it is too large
Load diff
1787
docs/superpowers/plans/2026-05-13-03-auth-logging.md
Normal file
1787
docs/superpowers/plans/2026-05-13-03-auth-logging.md
Normal file
File diff suppressed because it is too large
Load diff
1777
docs/superpowers/plans/2026-05-13-04-ai-pipelines.md
Normal file
1777
docs/superpowers/plans/2026-05-13-04-ai-pipelines.md
Normal file
File diff suppressed because it is too large
Load diff
1322
docs/superpowers/plans/2026-05-13-05-ebay-adapter.md
Normal file
1322
docs/superpowers/plans/2026-05-13-05-ebay-adapter.md
Normal file
File diff suppressed because it is too large
Load diff
1699
docs/superpowers/plans/2026-05-13-06-order-processing.md
Normal file
1699
docs/superpowers/plans/2026-05-13-06-order-processing.md
Normal file
File diff suppressed because it is too large
Load diff
44
phpunit.dist.xml
Normal file
44
phpunit.dist.xml
Normal 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>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
0
translations/.gitignore
vendored
Normal file
Loading…
Reference in a new issue