Merge pull request 'feat: add template version management' (#63) from feat/template-version-management into main
This commit is contained in:
commit
80c778cb08
6 changed files with 533 additions and 3 deletions
69
Migrations/0001_01_01_000007_add_template_versions.php
Normal file
69
Migrations/0001_01_01_000007_add_template_versions.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Create plan_template_versions table and add template_version_id to agent_plans.
|
||||
*
|
||||
* Template versions snapshot YAML template content at plan-creation time so
|
||||
* existing plans are never affected when a template file is updated.
|
||||
*
|
||||
* Deduplication: identical content reuses the same version row (same content_hash).
|
||||
*
|
||||
* Guarded with hasTable()/hasColumn() so this migration is idempotent and
|
||||
* can coexist with a consolidated app-level migration.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
if (! Schema::hasTable('plan_template_versions')) {
|
||||
Schema::create('plan_template_versions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('slug');
|
||||
$table->unsignedInteger('version');
|
||||
$table->string('name');
|
||||
$table->json('content');
|
||||
$table->char('content_hash', 64);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['slug', 'version']);
|
||||
$table->index(['slug', 'content_hash']);
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('agent_plans') && ! Schema::hasColumn('agent_plans', 'template_version_id')) {
|
||||
Schema::table('agent_plans', function (Blueprint $table) {
|
||||
$table->foreignId('template_version_id')
|
||||
->nullable()
|
||||
->constrained('plan_template_versions')
|
||||
->nullOnDelete()
|
||||
->after('source_file');
|
||||
});
|
||||
}
|
||||
|
||||
Schema::enableForeignKeyConstraints();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
if (Schema::hasTable('agent_plans') && Schema::hasColumn('agent_plans', 'template_version_id')) {
|
||||
Schema::table('agent_plans', function (Blueprint $table) {
|
||||
$table->dropForeign(['template_version_id']);
|
||||
$table->dropColumn('template_version_id');
|
||||
});
|
||||
}
|
||||
|
||||
Schema::dropIfExists('plan_template_versions');
|
||||
|
||||
Schema::enableForeignKeyConstraints();
|
||||
}
|
||||
};
|
||||
|
|
@ -66,6 +66,7 @@ class AgentPlan extends Model
|
|||
'metadata',
|
||||
'source_file',
|
||||
'archived_at',
|
||||
'template_version_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
|
@ -105,6 +106,11 @@ class AgentPlan extends Model
|
|||
return $this->hasMany(AgentWorkspaceState::class);
|
||||
}
|
||||
|
||||
public function templateVersion(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PlanTemplateVersion::class, 'template_version_id');
|
||||
}
|
||||
|
||||
// Scopes
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
|
|
|
|||
92
Models/PlanTemplateVersion.php
Normal file
92
Models/PlanTemplateVersion.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Agentic\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* Plan Template Version - immutable snapshot of a YAML template's content.
|
||||
*
|
||||
* When a plan is created from a template, the template content is snapshotted
|
||||
* here so future edits to the YAML file do not affect existing plans.
|
||||
*
|
||||
* Identical content is deduplicated via content_hash so no duplicate rows
|
||||
* accumulate when the same (unchanged) template is used repeatedly.
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $slug Template file slug (filename without extension)
|
||||
* @property int $version Sequential version number per slug
|
||||
* @property string $name Template name at snapshot time
|
||||
* @property array $content Full template content as JSON
|
||||
* @property string $content_hash SHA-256 of json_encode($content)
|
||||
* @property \Carbon\Carbon|null $created_at
|
||||
* @property \Carbon\Carbon|null $updated_at
|
||||
*/
|
||||
class PlanTemplateVersion extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'slug',
|
||||
'version',
|
||||
'name',
|
||||
'content',
|
||||
'content_hash',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'content' => 'array',
|
||||
'version' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Plans that were created from this template version.
|
||||
*/
|
||||
public function plans(): HasMany
|
||||
{
|
||||
return $this->hasMany(AgentPlan::class, 'template_version_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an existing version by content hash, or create a new one.
|
||||
*
|
||||
* Deduplicates identical template content so we don't store redundant rows
|
||||
* when the same (unchanged) template is used multiple times.
|
||||
*/
|
||||
public static function findOrCreateFromTemplate(string $slug, array $content): self
|
||||
{
|
||||
$hash = hash('sha256', json_encode($content, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$existing = static::where('slug', $slug)
|
||||
->where('content_hash', $hash)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$nextVersion = (static::where('slug', $slug)->max('version') ?? 0) + 1;
|
||||
|
||||
return static::create([
|
||||
'slug' => $slug,
|
||||
'version' => $nextVersion,
|
||||
'name' => $content['name'] ?? $slug,
|
||||
'content' => $content,
|
||||
'content_hash' => $hash,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all recorded versions for a template slug, newest first.
|
||||
*
|
||||
* @return Collection<int, static>
|
||||
*/
|
||||
public static function historyFor(string $slug): Collection
|
||||
{
|
||||
return static::where('slug', $slug)
|
||||
->orderByDesc('version')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ namespace Core\Mod\Agentic\Services;
|
|||
|
||||
use Core\Mod\Agentic\Models\AgentPhase;
|
||||
use Core\Mod\Agentic\Models\AgentPlan;
|
||||
use Core\Mod\Agentic\Models\PlanTemplateVersion;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
|
@ -146,6 +147,10 @@ class PlanTemplateService
|
|||
return null;
|
||||
}
|
||||
|
||||
// Snapshot the raw template content before variable substitution so the
|
||||
// version record captures the canonical template, not the instantiated copy.
|
||||
$templateVersion = PlanTemplateVersion::findOrCreateFromTemplate($templateSlug, $template);
|
||||
|
||||
// Replace variables in template
|
||||
$template = $this->substituteVariables($template, $variables);
|
||||
|
||||
|
|
@ -164,10 +169,12 @@ class PlanTemplateService
|
|||
'description' => $template['description'] ?? null,
|
||||
'context' => $context,
|
||||
'status' => ($options['activate'] ?? false) ? AgentPlan::STATUS_ACTIVE : AgentPlan::STATUS_DRAFT,
|
||||
'template_version_id' => $templateVersion->id,
|
||||
'metadata' => array_merge($template['metadata'] ?? [], [
|
||||
'source' => 'template',
|
||||
'template_slug' => $templateSlug,
|
||||
'template_name' => $template['name'],
|
||||
'template_version' => $templateVersion->version,
|
||||
'variables' => $variables,
|
||||
'created_at' => now()->toIso8601String(),
|
||||
]),
|
||||
|
|
@ -361,6 +368,7 @@ class PlanTemplateService
|
|||
}
|
||||
|
||||
/**
|
||||
<<<<<<< HEAD
|
||||
* Naming convention reminder included in validation results.
|
||||
*/
|
||||
private const NAMING_CONVENTION = 'Variable names use snake_case (e.g. project_name, api_key)';
|
||||
|
|
@ -401,6 +409,41 @@ class PlanTemplateService
|
|||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version history for a template slug, newest first.
|
||||
*
|
||||
* Returns an array of version summaries (without full content) for display.
|
||||
*
|
||||
* @return array<int, array{id: int, slug: string, version: int, name: string, content_hash: string, created_at: string}>
|
||||
*/
|
||||
public function getVersionHistory(string $slug): array
|
||||
{
|
||||
return PlanTemplateVersion::historyFor($slug)
|
||||
->map(fn (PlanTemplateVersion $v) => [
|
||||
'id' => $v->id,
|
||||
'slug' => $v->slug,
|
||||
'version' => $v->version,
|
||||
'name' => $v->name,
|
||||
'content_hash' => $v->content_hash,
|
||||
'created_at' => $v->created_at?->toIso8601String(),
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific stored version of a template by slug and version number.
|
||||
*
|
||||
* Returns the snapshotted content array, or null if not found.
|
||||
*/
|
||||
public function getVersion(string $slug, int $version): ?array
|
||||
{
|
||||
$record = PlanTemplateVersion::where('slug', $slug)
|
||||
->where('version', $version)
|
||||
->first();
|
||||
|
||||
return $record?->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get templates by category.
|
||||
*/
|
||||
|
|
|
|||
6
TODO.md
6
TODO.md
|
|
@ -191,10 +191,10 @@ Production-quality task list for the AI agent orchestration package.
|
|||
- Issue: Archived plans kept forever
|
||||
- Fix: Add configurable retention period, cleanup job
|
||||
|
||||
- [ ] **FEAT-003: Template version management**
|
||||
- Location: `Services/PlanTemplateService.php`
|
||||
- [x] **FEAT-003: Template version management**
|
||||
- Location: `Services/PlanTemplateService.php`, `Models/PlanTemplateVersion.php`
|
||||
- Issue: Template changes affect existing plan references
|
||||
- Fix: Add version tracking to templates
|
||||
- Fix: Add version tracking to templates — implemented in #35
|
||||
|
||||
### Consistency
|
||||
|
||||
|
|
|
|||
320
tests/Feature/TemplateVersionManagementTest.php
Normal file
320
tests/Feature/TemplateVersionManagementTest.php
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Tests for template version management (FEAT-003).
|
||||
*
|
||||
* Covers:
|
||||
* - Version snapshot creation when a plan is created from a template
|
||||
* - Deduplication: identical template content reuses the same version row
|
||||
* - New versions are created when template content changes
|
||||
* - Plans reference their template version
|
||||
* - Version history and retrieval via PlanTemplateService
|
||||
*/
|
||||
|
||||
use Core\Mod\Agentic\Models\AgentPlan;
|
||||
use Core\Mod\Agentic\Models\PlanTemplateVersion;
|
||||
use Core\Mod\Agentic\Services\PlanTemplateService;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
// =========================================================================
|
||||
// Setup helpers
|
||||
// =========================================================================
|
||||
|
||||
beforeEach(function () {
|
||||
$this->workspace = Workspace::factory()->create();
|
||||
$this->service = app(PlanTemplateService::class);
|
||||
$this->templatesPath = resource_path('plan-templates');
|
||||
|
||||
if (! File::isDirectory($this->templatesPath)) {
|
||||
File::makeDirectory($this->templatesPath, 0755, true);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (File::isDirectory($this->templatesPath)) {
|
||||
File::deleteDirectory($this->templatesPath);
|
||||
}
|
||||
});
|
||||
|
||||
function writeVersionTemplate(string $slug, array $content): void
|
||||
{
|
||||
File::put(
|
||||
resource_path('plan-templates').'/'.$slug.'.yaml',
|
||||
Yaml::dump($content, 10)
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PlanTemplateVersion model
|
||||
// =========================================================================
|
||||
|
||||
describe('PlanTemplateVersion model', function () {
|
||||
it('creates a new version record on first use', function () {
|
||||
$content = ['name' => 'My Template', 'slug' => 'my-tpl', 'phases' => []];
|
||||
|
||||
$version = PlanTemplateVersion::findOrCreateFromTemplate('my-tpl', $content);
|
||||
|
||||
expect($version)->toBeInstanceOf(PlanTemplateVersion::class)
|
||||
->and($version->slug)->toBe('my-tpl')
|
||||
->and($version->version)->toBe(1)
|
||||
->and($version->name)->toBe('My Template')
|
||||
->and($version->content)->toBe($content)
|
||||
->and($version->content_hash)->toBe(hash('sha256', json_encode($content, JSON_UNESCAPED_UNICODE)));
|
||||
});
|
||||
|
||||
it('reuses existing version when content is identical', function () {
|
||||
$content = ['name' => 'Stable', 'slug' => 'stable', 'phases' => []];
|
||||
|
||||
$v1 = PlanTemplateVersion::findOrCreateFromTemplate('stable', $content);
|
||||
$v2 = PlanTemplateVersion::findOrCreateFromTemplate('stable', $content);
|
||||
|
||||
expect($v1->id)->toBe($v2->id)
|
||||
->and(PlanTemplateVersion::where('slug', 'stable')->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('creates a new version when content changes', function () {
|
||||
$contentV1 = ['name' => 'Template', 'slug' => 'evolving', 'phases' => []];
|
||||
$contentV2 = ['name' => 'Template', 'slug' => 'evolving', 'phases' => [['name' => 'New Phase']]];
|
||||
|
||||
$v1 = PlanTemplateVersion::findOrCreateFromTemplate('evolving', $contentV1);
|
||||
$v2 = PlanTemplateVersion::findOrCreateFromTemplate('evolving', $contentV2);
|
||||
|
||||
expect($v1->id)->not->toBe($v2->id)
|
||||
->and($v1->version)->toBe(1)
|
||||
->and($v2->version)->toBe(2)
|
||||
->and(PlanTemplateVersion::where('slug', 'evolving')->count())->toBe(2);
|
||||
});
|
||||
|
||||
it('increments version numbers sequentially', function () {
|
||||
for ($i = 1; $i <= 3; $i++) {
|
||||
$content = ['name' => "Version {$i}", 'slug' => 'sequential', 'phases' => [$i]];
|
||||
$v = PlanTemplateVersion::findOrCreateFromTemplate('sequential', $content);
|
||||
expect($v->version)->toBe($i);
|
||||
}
|
||||
});
|
||||
|
||||
it('scopes versions by slug independently', function () {
|
||||
$alpha = ['name' => 'Alpha', 'slug' => 'alpha', 'phases' => []];
|
||||
$beta = ['name' => 'Beta', 'slug' => 'beta', 'phases' => []];
|
||||
|
||||
$vA = PlanTemplateVersion::findOrCreateFromTemplate('alpha', $alpha);
|
||||
$vB = PlanTemplateVersion::findOrCreateFromTemplate('beta', $beta);
|
||||
|
||||
expect($vA->version)->toBe(1)
|
||||
->and($vB->version)->toBe(1)
|
||||
->and($vA->id)->not->toBe($vB->id);
|
||||
});
|
||||
|
||||
it('returns history for a slug newest first', function () {
|
||||
$content1 = ['name' => 'T', 'slug' => 'hist', 'phases' => [1]];
|
||||
$content2 = ['name' => 'T', 'slug' => 'hist', 'phases' => [2]];
|
||||
|
||||
PlanTemplateVersion::findOrCreateFromTemplate('hist', $content1);
|
||||
PlanTemplateVersion::findOrCreateFromTemplate('hist', $content2);
|
||||
|
||||
$history = PlanTemplateVersion::historyFor('hist');
|
||||
|
||||
expect($history)->toHaveCount(2)
|
||||
->and($history[0]->version)->toBe(2)
|
||||
->and($history[1]->version)->toBe(1);
|
||||
});
|
||||
|
||||
it('returns empty collection when no versions exist for slug', function () {
|
||||
$history = PlanTemplateVersion::historyFor('nonexistent-slug');
|
||||
|
||||
expect($history)->toBeEmpty();
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Plan creation snapshots a template version
|
||||
// =========================================================================
|
||||
|
||||
describe('plan creation version tracking', function () {
|
||||
it('creates a version record when creating a plan from a template', function () {
|
||||
writeVersionTemplate('versioned-plan', [
|
||||
'name' => 'Versioned Plan',
|
||||
'phases' => [['name' => 'Phase 1']],
|
||||
]);
|
||||
|
||||
$plan = $this->service->createPlan('versioned-plan', [], [], $this->workspace);
|
||||
|
||||
expect($plan->template_version_id)->not->toBeNull()
|
||||
->and(PlanTemplateVersion::where('slug', 'versioned-plan')->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('associates the plan with the template version', function () {
|
||||
writeVersionTemplate('linked-version', [
|
||||
'name' => 'Linked Version',
|
||||
'phases' => [],
|
||||
]);
|
||||
|
||||
$plan = $this->service->createPlan('linked-version', [], [], $this->workspace);
|
||||
|
||||
expect($plan->templateVersion)->toBeInstanceOf(PlanTemplateVersion::class)
|
||||
->and($plan->templateVersion->slug)->toBe('linked-version')
|
||||
->and($plan->templateVersion->version)->toBe(1);
|
||||
});
|
||||
|
||||
it('stores template version number in metadata', function () {
|
||||
writeVersionTemplate('meta-version', [
|
||||
'name' => 'Meta Version',
|
||||
'phases' => [],
|
||||
]);
|
||||
|
||||
$plan = $this->service->createPlan('meta-version', [], [], $this->workspace);
|
||||
|
||||
expect($plan->metadata['template_version'])->toBe(1);
|
||||
});
|
||||
|
||||
it('reuses the same version record for multiple plans from unchanged template', function () {
|
||||
writeVersionTemplate('shared-version', [
|
||||
'name' => 'Shared Template',
|
||||
'phases' => [],
|
||||
]);
|
||||
|
||||
$plan1 = $this->service->createPlan('shared-version', [], [], $this->workspace);
|
||||
$plan2 = $this->service->createPlan('shared-version', [], [], $this->workspace);
|
||||
|
||||
expect($plan1->template_version_id)->toBe($plan2->template_version_id)
|
||||
->and(PlanTemplateVersion::where('slug', 'shared-version')->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('creates a new version when template content changes between plan creations', function () {
|
||||
writeVersionTemplate('changing-template', [
|
||||
'name' => 'Original',
|
||||
'phases' => [],
|
||||
]);
|
||||
|
||||
$plan1 = $this->service->createPlan('changing-template', [], [], $this->workspace);
|
||||
|
||||
// Simulate template file update
|
||||
writeVersionTemplate('changing-template', [
|
||||
'name' => 'Updated',
|
||||
'phases' => [['name' => 'Added Phase']],
|
||||
]);
|
||||
|
||||
// Re-resolve the service so it reads fresh YAML
|
||||
$freshService = new PlanTemplateService;
|
||||
$plan2 = $freshService->createPlan('changing-template', [], [], $this->workspace);
|
||||
|
||||
expect($plan1->template_version_id)->not->toBe($plan2->template_version_id)
|
||||
->and($plan1->templateVersion->version)->toBe(1)
|
||||
->and($plan2->templateVersion->version)->toBe(2)
|
||||
->and(PlanTemplateVersion::where('slug', 'changing-template')->count())->toBe(2);
|
||||
});
|
||||
|
||||
it('existing plans keep their version reference after template update', function () {
|
||||
writeVersionTemplate('stable-ref', [
|
||||
'name' => 'Stable',
|
||||
'phases' => [],
|
||||
]);
|
||||
|
||||
$plan = $this->service->createPlan('stable-ref', [], [], $this->workspace);
|
||||
$originalVersionId = $plan->template_version_id;
|
||||
|
||||
// Update template file
|
||||
writeVersionTemplate('stable-ref', [
|
||||
'name' => 'Stable Updated',
|
||||
'phases' => [['name' => 'New Phase']],
|
||||
]);
|
||||
|
||||
// Reload plan from DB - version reference must be unchanged
|
||||
$plan->refresh();
|
||||
|
||||
expect($plan->template_version_id)->toBe($originalVersionId)
|
||||
->and($plan->templateVersion->name)->toBe('Stable');
|
||||
});
|
||||
|
||||
it('snapshots raw template content before variable substitution', function () {
|
||||
writeVersionTemplate('raw-snapshot', [
|
||||
'name' => '{{ project }} Plan',
|
||||
'phases' => [['name' => '{{ project }} Setup']],
|
||||
]);
|
||||
|
||||
$plan = $this->service->createPlan('raw-snapshot', ['project' => 'MyApp'], [], $this->workspace);
|
||||
|
||||
// Version content should retain placeholders, not the substituted values
|
||||
$versionContent = $plan->templateVersion->content;
|
||||
|
||||
expect($versionContent['name'])->toBe('{{ project }} Plan')
|
||||
->and($versionContent['phases'][0]['name'])->toBe('{{ project }} Setup');
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// PlanTemplateService version methods
|
||||
// =========================================================================
|
||||
|
||||
describe('PlanTemplateService version methods', function () {
|
||||
it('getVersionHistory returns empty array when no plans created', function () {
|
||||
$history = $this->service->getVersionHistory('no-plans-yet');
|
||||
|
||||
expect($history)->toBeArray()->toBeEmpty();
|
||||
});
|
||||
|
||||
it('getVersionHistory returns version summaries after plan creation', function () {
|
||||
writeVersionTemplate('hist-service', [
|
||||
'name' => 'History Template',
|
||||
'phases' => [],
|
||||
]);
|
||||
|
||||
$this->service->createPlan('hist-service', [], [], $this->workspace);
|
||||
|
||||
$history = $this->service->getVersionHistory('hist-service');
|
||||
|
||||
expect($history)->toHaveCount(1)
|
||||
->and($history[0])->toHaveKeys(['id', 'slug', 'version', 'name', 'content_hash', 'created_at'])
|
||||
->and($history[0]['slug'])->toBe('hist-service')
|
||||
->and($history[0]['version'])->toBe(1)
|
||||
->and($history[0]['name'])->toBe('History Template');
|
||||
});
|
||||
|
||||
it('getVersionHistory orders newest version first', function () {
|
||||
writeVersionTemplate('order-hist', ['name' => 'V1', 'phases' => []]);
|
||||
$this->service->createPlan('order-hist', [], [], $this->workspace);
|
||||
|
||||
writeVersionTemplate('order-hist', ['name' => 'V2', 'phases' => [['name' => 'p']]]);
|
||||
$freshService = new PlanTemplateService;
|
||||
$freshService->createPlan('order-hist', [], [], $this->workspace);
|
||||
|
||||
$history = $this->service->getVersionHistory('order-hist');
|
||||
|
||||
expect($history[0]['version'])->toBe(2)
|
||||
->and($history[1]['version'])->toBe(1);
|
||||
});
|
||||
|
||||
it('getVersion returns content for an existing version', function () {
|
||||
$templateContent = ['name' => 'Get Version Test', 'phases' => []];
|
||||
writeVersionTemplate('get-version', $templateContent);
|
||||
|
||||
$this->service->createPlan('get-version', [], [], $this->workspace);
|
||||
|
||||
$content = $this->service->getVersion('get-version', 1);
|
||||
|
||||
expect($content)->not->toBeNull()
|
||||
->and($content['name'])->toBe('Get Version Test');
|
||||
});
|
||||
|
||||
it('getVersion returns null for nonexistent version', function () {
|
||||
$content = $this->service->getVersion('nonexistent', 99);
|
||||
|
||||
expect($content)->toBeNull();
|
||||
});
|
||||
|
||||
it('getVersion returns the correct historic content when template has changed', function () {
|
||||
writeVersionTemplate('historic-content', ['name' => 'Original Name', 'phases' => []]);
|
||||
$this->service->createPlan('historic-content', [], [], $this->workspace);
|
||||
|
||||
writeVersionTemplate('historic-content', ['name' => 'Updated Name', 'phases' => [['name' => 'p']]]);
|
||||
$freshService = new PlanTemplateService;
|
||||
$freshService->createPlan('historic-content', [], [], $this->workspace);
|
||||
|
||||
expect($this->service->getVersion('historic-content', 1)['name'])->toBe('Original Name')
|
||||
->and($this->service->getVersion('historic-content', 2)['name'])->toBe('Updated Name');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue