@@ -42,13 +42,17 @@ import (
4242 "github.com/snyk/go-application-framework/pkg/runtimeinfo"
4343 "github.com/snyk/go-application-framework/pkg/workflow"
4444
45+ "github.com/snyk/snyk-ls/application/codeaction"
4546 "github.com/snyk/snyk-ls/application/config"
4647 "github.com/snyk/snyk-ls/application/di"
48+ "github.com/snyk/snyk-ls/application/watcher"
4749 mock_command "github.com/snyk/snyk-ls/domain/ide/command/mock"
4850 "github.com/snyk/snyk-ls/domain/ide/converter"
4951 "github.com/snyk/snyk-ls/domain/ide/hover"
5052 "github.com/snyk/snyk-ls/domain/ide/workspace"
5153 "github.com/snyk/snyk-ls/domain/snyk"
54+ "github.com/snyk/snyk-ls/domain/snyk/mock_snyk"
55+ "github.com/snyk/snyk-ls/domain/snyk/remediation"
5256 "github.com/snyk/snyk-ls/domain/snyk/scanner"
5357 "github.com/snyk/snyk-ls/infrastructure/authentication"
5458 "github.com/snyk/snyk-ls/infrastructure/cli"
@@ -1822,3 +1826,173 @@ func TestInitializeHandler_MissingDep_PropagatesLSPError(t *testing.T) {
18221826 })
18231827 }
18241828}
1829+
1830+ // Test_textDocumentDidChange_WithRemediationEnabled_NoRPCError verifies that a
1831+ // textDocument/didChange notification reaches the onFileChange callback and
1832+ // returns no RPC error when the remediation agent is enabled (INTEG-006).
1833+ func Test_textDocumentDidChange_WithRemediationEnabled_NoRPCError (t * testing.T ) {
1834+ engine , tokenService := testutil .UnitTestWithEngine (t )
1835+ engine .GetConfiguration ().Set ("remediation_agent_enabled" , true )
1836+ loc , _ , _ := setupServer (t , engine , tokenService )
1837+ testutil .CreateDummyProgressListener (t )
1838+
1839+ dir := t .TempDir ()
1840+ file := testsupport .CreateTempFile (t , dir )
1841+ fileURI := uri .PathToUri (types .FilePath (file .Name ()))
1842+ folderURI := uri .PathToUri (types .FilePath (dir ))
1843+
1844+ // Add the folder so the file is inside a known workspace folder.
1845+ _ , err := loc .Client .Call (t .Context (), "workspace/didChangeWorkspaceFolders" , types.DidChangeWorkspaceFoldersParams {
1846+ Event : types.WorkspaceFoldersChangeEvent {
1847+ Added : []types.WorkspaceFolder {{Name : dir , Uri : folderURI }},
1848+ },
1849+ })
1850+ require .NoError (t , err )
1851+
1852+ // didChange must reach onFileChange and return no RPC error.
1853+ _ , err = loc .Client .Call (t .Context (), "textDocument/didChange" , sglsp.DidChangeTextDocumentParams {
1854+ TextDocument : sglsp.VersionedTextDocumentIdentifier {TextDocumentIdentifier : sglsp.TextDocumentIdentifier {URI : fileURI }, Version : 1 },
1855+ ContentChanges : []sglsp.TextDocumentContentChangeEvent {{Text : "package main\n \n func main() {}\n " }},
1856+ })
1857+ require .NoError (t , err , "textDocument/didChange must not return an RPC error" )
1858+ }
1859+
1860+ // Test_workspaceDidChangeWorkspaceFolders_RemediationAction_WorksInDynamicFolder verifies that a
1861+ // textDocument/codeAction request against a file in a folder added via workspace/didChangeWorkspaceFolders
1862+ // succeeds without error (INTEG-005: worktree-root code-action reachability).
1863+ func Test_workspaceDidChangeWorkspaceFolders_RemediationAction_WorksInDynamicFolder (t * testing.T ) {
1864+ engine , tokenService := testutil .UnitTestWithEngine (t )
1865+ // Enable the remediation-agent flag so the provider is wired in by TestInit path.
1866+ engine .GetConfiguration ().Set ("remediation_agent_enabled" , true )
1867+ loc , _ , _ := setupServer (t , engine , tokenService )
1868+ testutil .CreateDummyProgressListener (t )
1869+
1870+ dir := t .TempDir ()
1871+ file := testsupport .CreateTempFile (t , dir )
1872+ filePath := types .FilePath (file .Name ())
1873+ folderUri := uri .PathToUri (types .FilePath (dir ))
1874+
1875+ // Add the folder dynamically, mirroring what ambient-canary does for a worktree root.
1876+ _ , err := loc .Client .Call (t .Context (), "workspace/didChangeWorkspaceFolders" , types.DidChangeWorkspaceFoldersParams {
1877+ Event : types.WorkspaceFoldersChangeEvent {
1878+ Added : []types.WorkspaceFolder {{Name : dir , Uri : folderUri }},
1879+ },
1880+ })
1881+ require .NoError (t , err )
1882+
1883+ // textDocument/codeAction must return a valid (possibly empty) response — no RPC error.
1884+ params := types.CodeActionParams {
1885+ TextDocument : sglsp.TextDocumentIdentifier {URI : uri .PathToUri (filePath )},
1886+ Range : sglsp.Range {Start : sglsp.Position {Line : 0 , Character : 0 }, End : sglsp.Position {Line : 0 , Character : 1 }},
1887+ }
1888+ resp , err := loc .Client .Call (t .Context (), "textDocument/codeAction" , params )
1889+ require .NoError (t , err , "codeAction must not return an RPC error for a dynamically-added folder" )
1890+
1891+ var actions []types.LSPCodeAction
1892+ require .NoError (t , resp .UnmarshalResult (& actions ))
1893+ // No assertion on count: the file is empty, so no issues and no actions.
1894+ // The test's value is that the call succeeds rather than returning a "folder not found" error.
1895+ }
1896+
1897+ // serverTestRemediationProvider is a test double for remediation.RemediationProvider
1898+ // used in server-level integration tests.
1899+ type serverTestRemediationProvider struct {
1900+ edit * types.WorkspaceEdit
1901+ }
1902+
1903+ func (f * serverTestRemediationProvider ) Remediate (_ context.Context , _ remediation.RemediationRequest ) (* types.WorkspaceEdit , error ) {
1904+ return f .edit , nil
1905+ }
1906+
1907+ // Test_codeActionResolve_RemediationAgent_ReturnsEdit verifies the full LSP
1908+ // round-trip for a RemediationAgentQuickFix code action:
1909+ // 1. textDocument/codeAction returns an action with kind=RemediationAgentQuickFix,
1910+ // nil edit, and a UUID in Data.
1911+ // 2. codeAction/resolve calls through to the remediation provider and returns
1912+ // the WorkspaceEdit (INTEG-007).
1913+ func Test_codeActionResolve_RemediationAgent_ReturnsEdit (t * testing.T ) {
1914+ engine , tokenService := testutil .UnitTestWithEngine (t )
1915+ testutil .CreateDummyProgressListener (t )
1916+
1917+ dir := t .TempDir ()
1918+ file := testsupport .CreateTempFile (t , dir )
1919+ filePath := types .FilePath (file .Name ())
1920+ fileURI := uri .PathToUri (filePath )
1921+ folderURI := uri .PathToUri (types .FilePath (dir ))
1922+
1923+ // Fixable Code issue with FindingId — the one that triggers RemediationAgentQuickFix.
1924+ fixableIssue := & snyk.Issue {
1925+ FindingId : "finding-integ-resolve" ,
1926+ Product : product .ProductCode ,
1927+ AdditionalData : snyk.CodeIssueData {HasAIFix : true },
1928+ }
1929+
1930+ // Mock issue provider returns the fixable issue for any range query.
1931+ ctrl := gomock .NewController (t )
1932+ issueProvider := mock_snyk .NewMockIssueProvider (ctrl )
1933+ issueProvider .EXPECT ().IssuesForRange (gomock .Any (), gomock .Any ()).Return ([]types.Issue {fixableIssue }).AnyTimes ()
1934+
1935+ // Fake remediation provider returns a pre-built WorkspaceEdit.
1936+ mockEdit := & types.WorkspaceEdit {
1937+ Changes : map [string ][]types.TextEdit {
1938+ string (filePath ): {{NewText : "// fixed by remy\n " }},
1939+ },
1940+ }
1941+ fakeProvider := & serverTestRemediationProvider {edit : mockEdit }
1942+
1943+ // Build a custom CodeActionsService wired with the mocks.
1944+ fw := watcher .NewFileWatcher ()
1945+ customCAService := codeaction .NewService (
1946+ engine , issueProvider , fw ,
1947+ notification .NewMockNotifier (),
1948+ featureflag .NewFakeService (),
1949+ types .NewConfigResolver (engine .GetLogger ()),
1950+ fakeProvider ,
1951+ )
1952+
1953+ // Start server with the custom CodeActionsService injected via deps override.
1954+ baseDeps := di .TestInit (t , engine , tokenService , nil )
1955+ baseDeps .CodeActionService = customCAService
1956+ jsonRPCRecorder := & testsupport.JsonRPCRecorder {}
1957+ loc := startServer (engine , tokenService , nil , jsonRPCRecorder , baseDeps )
1958+ t .Cleanup (func () { _ = loc .Close () })
1959+
1960+ // Register the workspace folder so GetFolderContaining finds the file.
1961+ _ , err := loc .Client .Call (t .Context (), "workspace/didChangeWorkspaceFolders" , types.DidChangeWorkspaceFoldersParams {
1962+ Event : types.WorkspaceFoldersChangeEvent {
1963+ Added : []types.WorkspaceFolder {{Name : dir , Uri : folderURI }},
1964+ },
1965+ })
1966+ require .NoError (t , err )
1967+
1968+ // List code actions for the file range.
1969+ caParams := types.CodeActionParams {
1970+ TextDocument : sglsp.TextDocumentIdentifier {URI : fileURI },
1971+ Range : sglsp.Range {Start : sglsp.Position {Line : 0 , Character : 0 }, End : sglsp.Position {Line : 0 , Character : 1 }},
1972+ }
1973+ caResp , err := loc .Client .Call (t .Context (), "textDocument/codeAction" , caParams )
1974+ require .NoError (t , err )
1975+
1976+ var listedActions []types.LSPCodeAction
1977+ require .NoError (t , caResp .UnmarshalResult (& listedActions ))
1978+
1979+ var remyAction * types.LSPCodeAction
1980+ for i := range listedActions {
1981+ if listedActions [i ].Kind == types .RemediationAgentQuickFix {
1982+ remyAction = & listedActions [i ]
1983+ break
1984+ }
1985+ }
1986+ require .NotNil (t , remyAction , "expected a RemediationAgentQuickFix action in the list response" )
1987+ assert .Nil (t , remyAction .Edit , "edit must be nil at list time (deferred)" )
1988+ require .NotNil (t , remyAction .Data , "Data must carry the deferred-resolve UUID" )
1989+
1990+ // Resolve the code action through the full LSP stack.
1991+ resolveResp , err := loc .Client .Call (t .Context (), "codeAction/resolve" , * remyAction )
1992+ require .NoError (t , err )
1993+
1994+ var resolved types.LSPCodeAction
1995+ require .NoError (t , resolveResp .UnmarshalResult (& resolved ))
1996+ require .NotNil (t , resolved .Edit , "resolved action must carry the WorkspaceEdit from the provider" )
1997+ assert .NotEmpty (t , resolved .Edit .Changes , "WorkspaceEdit must have at least one change" )
1998+ }
0 commit comments