@@ -24,6 +24,7 @@ package acceptance_test
2424
2525import (
2626 "encoding/json"
27+ "fmt"
2728 "os"
2829 "path/filepath"
2930 "testing"
@@ -248,6 +249,48 @@ func TestConfigValidate_InfersOrgFromGitRemote(t *testing.T) {
248249 assert .Check (t , cmp .Equal (fake .LastCompileOwnerID (), testOrgUUID ))
249250}
250251
252+ // TestConfigValidate_ProjectLinkOverridesGitRemote pins that org inference
253+ // honours a `circleci project link` binding (.circleci/info.yml) over the git
254+ // remote — the case where the remote is not the right answer (repository
255+ // renames, forks, standalone projects). The remote points at one org; the link
256+ // binds a different one, and the linked org must reach the compile endpoint.
257+ //
258+ // The linked project is deliberately NOT registered with the fake: `project
259+ // link` records the org UUID in info.yml, so inference uses it directly with no
260+ // project lookup. The linked org resolving without a matching fake project
261+ // proves that direct-ID path (which also works offline / with a restricted
262+ // token).
263+ func TestConfigValidate_ProjectLinkOverridesGitRemote (t * testing.T ) {
264+ const linkedOrgUUID = "00000000-0000-0000-0000-0000000000cc"
265+ const linkedProjUUID = "00000000-0000-0000-0000-0000000000dd"
266+
267+ fake := fakes .NewCircleCI (t )
268+ fake .SetCompileResponse (true , testCompiledYAML )
269+ // The git remote resolves to a *different* org. If the link were ignored,
270+ // this org would be used and the assertion below would catch it.
271+ fake .AddProjectBySlug ("gh/otherorg/otherrepo" , "00000000-0000-0000-0000-0000000000b3" , "otherrepo" , testOrgUUID )
272+
273+ env := testenv .New (t )
274+ env .Token = testToken
275+ env .CircleCIURL = fake .URL ()
276+
277+ dir := t .TempDir ()
278+ initGitRepoWithRemote (t , dir , "https://github.com/otherorg/otherrepo" )
279+ writeConfig (t , dir , testConfigYAML )
280+ writeProjectLink (t , dir , linkedOrgUUID , linkedProjUUID )
281+
282+ result := binary .RunCLI (t , binary.RunOpts {
283+ Binary : binaryPath ,
284+ Args : []string {"config" , "validate" , "--config" , ".circleci/config.yml" },
285+ Env : env .Environ (),
286+ WorkDir : dir ,
287+ })
288+
289+ assert .Check (t , cmp .Equal (result .ExitCode , 0 ), "stderr: %s" , result .Stderr )
290+ // The linked project's org — not the git remote's — reached compile.
291+ assert .Check (t , cmp .Equal (fake .LastCompileOwnerID (), linkedOrgUUID ))
292+ }
293+
251294// TestConfigValidate_NoOrgOutsideGitRemote guards the other half of the #1061
252295// contract: inference is best-effort. Outside a git checkout (and with no
253296// --org) validation still succeeds, compiling with an empty owner_id rather
@@ -447,6 +490,18 @@ func writeConfig(t *testing.T, dir, content string) {
447490 writeFile (t , filepath .Join (dir , ".circleci" , "config.yml" ), content )
448491}
449492
493+ // writeProjectLink writes the .circleci/info.yml that `circleci project link`
494+ // produces, binding the checkout to a CircleCI project by its stable UUIDs.
495+ func writeProjectLink (t * testing.T , dir , orgID , projectID string ) {
496+ t .Helper ()
497+ assert .NilError (t , os .MkdirAll (filepath .Join (dir , ".circleci" ), 0o755 ))
498+ content := fmt .Sprintf (
499+ "organization:\n id: %s\n project:\n id: %s\n slug: circleci/%s/%s\n " ,
500+ orgID , projectID , orgID , projectID ,
501+ )
502+ writeFile (t , filepath .Join (dir , ".circleci" , "info.yml" ), content )
503+ }
504+
450505func writeFile (t * testing.T , path , content string ) {
451506 t .Helper ()
452507 assert .NilError (t , os .WriteFile (path , []byte (content ), 0o644 ))
0 commit comments