feat(worker): PowerUse-Addendum an die Handler-Prompts, Cap aus den Settings
HandoffRoundTracker.Register nimmt den Cap jetzt als Parameter statt aus einer Konstante; HandoffMcpTools liest ihn pro Aufruf aus app_settings. Werte unter 1 werden auf 1 geklemmt (in WorkerHub UND in Register): der Cap ist das Einzige, was eine entlaufene Handoff-Kette begrenzt, "0 = unbegrenzt" darf kein Aufrufer waehlen koennen. InteractiveLaunchSpecService.ComposeHandlerPromptAsync haengt das PowerUse- Addendum an den Phasen-Prompt, aber nur wenn globaler Schalter und Listen-Opt-in beide gesetzt sind. Bewusst ein Addendum statt einer zweiten Prompt-Garnitur: die gemeinsamen 95% bleiben an einer Stelle, und ein Run ohne PowerUse bekommt einen byte-identischen Prompt wie vorher. Das Addendum ist als Override der interaktiven Regeln formuliert (nie fragen, entscheiden und protokollieren; Konflikte loesen oder abbrechen, nie im Tree liegen lassen; Arbeit ernten, wenn eine Runde sonst leer laeuft). Drei Tests pinnen alle vier Ecken des UND, weil die Sicherheitsaussage des Features genau daran haengt.
This commit is contained in:
@@ -622,6 +622,97 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
||||
return dir;
|
||||
}
|
||||
|
||||
// --- PowerUse composition -------------------------------------------------------------------
|
||||
// PowerUse needs BOTH the global switch and the list's opt-in. These three tests pin all four
|
||||
// corners of that AND, because the whole safety story of the feature is that the global switch
|
||||
// alone cannot make a list unattended.
|
||||
|
||||
private async Task SetPowerUseEnabledAsync(bool value)
|
||||
{
|
||||
using var ctx = _db.CreateContext();
|
||||
await new AppSettingsRepository(ctx).GetAsync();
|
||||
var row = await ctx.AppSettings.FirstAsync(s => s.Id == AppSettingsEntity.SingletonId);
|
||||
row.PowerUseEnabled = value;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task SetPowerUseAllowedAsync(string listId, bool value)
|
||||
{
|
||||
using var ctx = _db.CreateContext();
|
||||
await new ListRepository(ctx).SetConfigAsync(new ListConfigEntity
|
||||
{
|
||||
ListId = listId,
|
||||
PowerUseAllowed = value,
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<string> ReadHandlerSystemPromptAsync(string listId, string taskId)
|
||||
{
|
||||
var svc = BuildService();
|
||||
var spec = await svc.BuildForMergeHelperAsync(
|
||||
new[] { taskId }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
||||
var sessionDir = TrackSessionDir(spec);
|
||||
return await File.ReadAllTextAsync(Path.Combine(sessionDir, "system-prompt.md"));
|
||||
}
|
||||
|
||||
private static string AddendumMarker =>
|
||||
PromptFiles.DefaultFor(PromptKind.PowerUseAddendum).Split('\n')[0].Trim();
|
||||
|
||||
[Fact]
|
||||
public async Task BuildForMergeHelperAsync_PowerUseOffGlobally_LeavesPromptUntouched()
|
||||
{
|
||||
var repo = Path.Combine(_tempDir, "puOffGlobal");
|
||||
Directory.CreateDirectory(repo);
|
||||
var listId = await SeedListAsync(workingDir: repo);
|
||||
var taskId = Guid.NewGuid().ToString();
|
||||
await SeedTaskAsync(taskId, listId, TaskStatus.WaitingForReview);
|
||||
|
||||
await SetPowerUseEnabledAsync(false);
|
||||
await SetPowerUseAllowedAsync(listId, true);
|
||||
|
||||
var prompt = await ReadHandlerSystemPromptAsync(listId, taskId);
|
||||
|
||||
Assert.DoesNotContain(AddendumMarker, prompt);
|
||||
Assert.Equal(PromptFiles.ReadOrDefault(PromptKind.MergeHelperTriage), prompt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildForMergeHelperAsync_PowerUseOnButListDidNotOptIn_LeavesPromptUntouched()
|
||||
{
|
||||
var repo = Path.Combine(_tempDir, "puNoOptIn");
|
||||
Directory.CreateDirectory(repo);
|
||||
var listId = await SeedListAsync(workingDir: repo);
|
||||
var taskId = Guid.NewGuid().ToString();
|
||||
await SeedTaskAsync(taskId, listId, TaskStatus.WaitingForReview);
|
||||
|
||||
await SetPowerUseEnabledAsync(true);
|
||||
// No list config row at all — the common case for an unrelated list.
|
||||
|
||||
var prompt = await ReadHandlerSystemPromptAsync(listId, taskId);
|
||||
|
||||
Assert.DoesNotContain(AddendumMarker, prompt);
|
||||
Assert.Equal(PromptFiles.ReadOrDefault(PromptKind.MergeHelperTriage), prompt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildForMergeHelperAsync_PowerUseOnAndListOptedIn_AppendsAddendum()
|
||||
{
|
||||
var repo = Path.Combine(_tempDir, "puOn");
|
||||
Directory.CreateDirectory(repo);
|
||||
var listId = await SeedListAsync(workingDir: repo);
|
||||
var taskId = Guid.NewGuid().ToString();
|
||||
await SeedTaskAsync(taskId, listId, TaskStatus.WaitingForReview);
|
||||
|
||||
await SetPowerUseEnabledAsync(true);
|
||||
await SetPowerUseAllowedAsync(listId, true);
|
||||
|
||||
var prompt = await ReadHandlerSystemPromptAsync(listId, taskId);
|
||||
|
||||
Assert.Contains(AddendumMarker, prompt);
|
||||
// The addendum overrides the base prompt, so it has to come AFTER it, not replace it.
|
||||
Assert.StartsWith(PromptFiles.ReadOrDefault(PromptKind.MergeHelperTriage), prompt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildForMergeHelperAsync_EmptyTaskIds_ThrowsInvalidOperation()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user