-
Notifications
You must be signed in to change notification settings - Fork 863
[release/13.2] Fix Playwright CLI provenance verification for tag format change #16134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,166 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Licensed to the .NET Foundation under one or more agreements. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The .NET Foundation licenses this file to you under the MIT license. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Aspire.Cli.EndToEnd.Tests.Helpers; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Aspire.Cli.Tests.Utils; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Hex1b.Automation; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Aspire.TestUtilities; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Xunit; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace Aspire.Cli.EndToEnd.Tests; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// End-to-end test verifying that the <c>aspire new</c> flow with AI agent initialization | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// completes without provenance verification errors when installing <c>@playwright/cli</c>. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| [OuterloopTest("Requires npm and network access to install @playwright/cli from the npm registry")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| public sealed class NewWithAgentInitTests(ITestOutputHelper output) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Exercises the full <c>aspire new</c> → agent init → Playwright CLI install flow end-to-end. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// This is the primary regression test for provenance verification failures (e.g., tag format changes | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// in upstream <c>@playwright/cli</c> releases). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// The test: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 1. Runs <c>aspire new</c> to create a Starter project | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 2. Accepts the agent init prompt (instead of declining) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 3. Selects Playwright CLI during skill selection | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 4. Verifies no errors appear (especially no "Provenance verification failed") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 5. Verifies <c>playwright-cli</c> is installed and skill files are generated | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| [Fact] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task AspireNew_WithAgentInit_InstallsPlaywrightWithoutErrors() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var repoRoot = CliE2ETestHelpers.GetRepoRoot(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var workspace = TemporaryWorkspace.Create(output); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| var counter = new SequenceCounter(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.PrepareDockerEnvironmentAsync(counter, workspace); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.InstallAspireCliInDockerAsync(installMode, counter); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create .claude folder so agent init detects a Claude Code environment. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // This needs to exist in the workspace root before aspire new creates the project | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // because agent init chains after project creation and looks for environment markers. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.TypeAsync("mkdir -p .claude"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.EnterAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.WaitForSuccessPromptAsync(counter); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Run aspire new with the Starter template, going through all prompts manually | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // so we can ACCEPT the agent init prompt instead of declining it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.TypeAsync("aspire new"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.EnterAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Template selection: accept default Starter App | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.WaitUntilAsync( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| s => new CellPatternSearcher().Find("> Starter App").Search(s).Count > 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout: TimeSpan.FromSeconds(60), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "template selection list (> Starter App)"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.EnterAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Project name | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.WaitUntilAsync( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| s => new CellPatternSearcher().Find("Enter the project name").Search(s).Count > 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout: TimeSpan.FromSeconds(10), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "project name prompt"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.TypeAsync("StarterApp"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.EnterAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Output path: accept default | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.WaitUntilAsync( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| s => new CellPatternSearcher().Find("Enter the output path").Search(s).Count > 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout: TimeSpan.FromSeconds(10), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "output path prompt"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.EnterAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // URLs prompt: accept default No | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.WaitUntilAsync( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| s => new CellPatternSearcher().Find("Use *.dev.localhost URLs").Search(s).Count > 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout: TimeSpan.FromSeconds(10), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "URLs prompt"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.EnterAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Redis cache: accept default Yes | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.WaitUntilAsync( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| s => new CellPatternSearcher().Find("Use Redis Cache").Search(s).Count > 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout: TimeSpan.FromSeconds(10), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "Redis cache prompt"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.EnterAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Test project: accept default No | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.WaitUntilAsync( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| s => new CellPatternSearcher().Find("Do you want to create a test project?").Search(s).Count > 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout: TimeSpan.FromSeconds(10), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "test project prompt"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.EnterAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Agent init prompt: ACCEPT it (type 'y') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.WaitUntilAsync( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| s => s.ContainsText("configure AI agent environments"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout: TimeSpan.FromSeconds(120), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "agent init prompt after aspire new"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.WaitAsync(500); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.TypeAsync("y"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.EnterAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Agent init: workspace path - accept default | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.WaitUntilTextAsync("workspace:", timeout: TimeSpan.FromSeconds(30)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.WaitAsync(500); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.EnterAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Agent init: skill location - select Claude Code | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.WaitUntilAsync( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| s => s.ContainsText("skill files be installed"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout: TimeSpan.FromSeconds(60), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "skill location prompt"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.TypeAsync(" "); // Toggle off default Standard location | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.DownAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.TypeAsync(" "); // Toggle on Claude Code location | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.EnterAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Agent init: skill selection - toggle on Playwright CLI | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.WaitUntilAsync( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| s => s.ContainsText("skills should be installed"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout: TimeSpan.FromSeconds(30), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "skill selection prompt"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.DownAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.TypeAsync(" "); // Toggle on Playwright CLI | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await auto.EnterAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+117
to
+135
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Agent init: skill location - select Claude Code | |
| await auto.WaitUntilAsync( | |
| s => s.ContainsText("skill files be installed"), | |
| timeout: TimeSpan.FromSeconds(60), | |
| description: "skill location prompt"); | |
| await auto.TypeAsync(" "); // Toggle off default Standard location | |
| await auto.DownAsync(); | |
| await auto.TypeAsync(" "); // Toggle on Claude Code location | |
| await auto.EnterAsync(); | |
| // Agent init: skill selection - toggle on Playwright CLI | |
| await auto.WaitUntilAsync( | |
| s => s.ContainsText("skills should be installed"), | |
| timeout: TimeSpan.FromSeconds(30), | |
| description: "skill selection prompt"); | |
| await auto.DownAsync(); | |
| await auto.TypeAsync(" "); // Toggle on Playwright CLI | |
| await auto.EnterAsync(); | |
| // Agent init: select the Playwright CLI configuration option from the current menu-based flow. | |
| await auto.WaitUntilAsync( | |
| s => s.ContainsText("What would you like to configure?") && s.ContainsText("Playwright CLI"), | |
| timeout: TimeSpan.FromSeconds(60), | |
| description: "agent init configuration prompt"); | |
| await auto.DownAsync(); | |
| await auto.EnterAsync(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -343,6 +343,40 @@ public void VerifyIntegrity_WithNonSha512Prefix_ReturnsFalse() | |
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task InstallAsync_WorkflowRefValidator_AcceptsBothTagFormats() | ||
| { | ||
| var version = SemVersion.Parse("0.1.7", SemVersionStyles.Strict); | ||
| var npmRunner = new TestNpmRunner | ||
| { | ||
| ResolveResult = new NpmPackageInfo { Version = version, Integrity = "sha512-abc123" } | ||
| }; | ||
| var provenanceChecker = new TestNpmProvenanceChecker(); | ||
| var playwrightRunner = new TestPlaywrightCliRunner(); | ||
| var installer = new PlaywrightCliInstaller(npmRunner, provenanceChecker, playwrightRunner, new TestInteractionService(), new ConfigurationBuilder().Build(), NullLogger<PlaywrightCliInstaller>.Instance); | ||
|
|
||
| await installer.InstallAsync(CreateTestContext(), CancellationToken.None); | ||
|
|
||
| Assert.True(provenanceChecker.ProvenanceCalled); | ||
| Assert.NotNull(provenanceChecker.CapturedValidateWorkflowRef); | ||
|
|
||
| // Accept tags without 'v' prefix (0.1.7+) | ||
| Assert.True(WorkflowRefInfo.TryParse($"refs/tags/{version}", out var refWithout)); | ||
| Assert.True(provenanceChecker.CapturedValidateWorkflowRef(refWithout!)); | ||
|
|
||
| // Accept tags with 'v' prefix (pre-0.1.7) | ||
| Assert.True(WorkflowRefInfo.TryParse($"refs/tags/v{version}", out var refWith)); | ||
| Assert.True(provenanceChecker.CapturedValidateWorkflowRef(refWith!)); | ||
|
|
||
| // Reject wrong version | ||
| Assert.True(WorkflowRefInfo.TryParse("refs/tags/0.2.0", out var wrongVersion)); | ||
| Assert.False(provenanceChecker.CapturedValidateWorkflowRef(wrongVersion!)); | ||
|
|
||
| // Reject branch ref (not a tag) | ||
| Assert.True(WorkflowRefInfo.TryParse("refs/heads/main", out var branchRef)); | ||
| Assert.False(provenanceChecker.CapturedValidateWorkflowRef(branchRef!)); | ||
| } | ||
|
Comment on lines
+346
to
+378
|
||
|
|
||
| [Fact] | ||
| public async Task InstallAsync_WhenProvenanceCheckFails_ReturnsFalse() | ||
JamesNK marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
|
|
@@ -577,10 +611,12 @@ private sealed class TestNpmProvenanceChecker : INpmProvenanceChecker | |
| { | ||
| public ProvenanceVerificationOutcome ProvenanceOutcome { get; set; } = ProvenanceVerificationOutcome.Verified; | ||
| public bool ProvenanceCalled { get; private set; } | ||
| public Func<WorkflowRefInfo, bool>? CapturedValidateWorkflowRef { get; private set; } | ||
|
|
||
| public Task<ProvenanceVerificationResult> VerifyProvenanceAsync(string packageName, string version, string expectedSourceRepository, string expectedWorkflowPath, string expectedBuildType, Func<WorkflowRefInfo, bool>? validateWorkflowRef, CancellationToken cancellationToken, string? sriIntegrity = null) | ||
| { | ||
| ProvenanceCalled = true; | ||
| CapturedValidateWorkflowRef = validateWorkflowRef; | ||
| return Task.FromResult(new ProvenanceVerificationResult | ||
| { | ||
| Outcome = ProvenanceOutcome, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test setup creates a
.claudefolder in the workspace root before runningaspire new, butNewCommandchains agent init using the template output directory as the workspace root. This means the Claude Code scanner can end up detecting the parent.claudeand writing config/skill files outside the newly created project directory, making the test less representative and potentially fragile if scanner boundary behavior changes.