Repository: JanDeDobbeleer/oh-my-posh
Pull request #4650 (2024-01-28 19:05:34)

Pulumi segment
### Prerequisites

- [x] I have read and understood the [contributing guide][CONTRIBUTING.md].
- [x] The commit message follows the [conventional commits][cc] guidelines.
- [x] Tests for the changes have been added (for bug fixes / features).
- [x] Docs have been added/updated (for bug fixes / features).

### Description
The aim of this PR is to introduces a new segment for pulumi.
Hope to have put everything in the right place!
The pulumi segment will display user (when a user is logged in), url (for remote state) and the stack selected (if a stack is selected).
The PR modifies the following file: `src/engine/segment.go`, `themes/schema.json`, and `website/sidebars.js` and adds new files `src/segments/pulumi.go`, `src/segments/pulumi_test.go`, and `website/docs/segments/pulumi.mdx`
<!---

Tips:

If you're not comfortable with working with Git,
we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary


kema-dev: Great idea, I would like to use this segment too !
Did you experience any performance issue ? I noticed that `pulumi about` is quite slow on my system (about 1.5 sec) and wonder if this is an issue with a om-my-posh segment ?
JanDeDobbeleer: > wonder if this is an issue with a om-my-posh segment

@kema-dev definitely, this would need to be cached
lzecca78: @JanDeDobbeleer @kema-dev  you are perfectly right, the issue with the cache, is the cache itself. It could be reasonably be enabled a sort of cache based on the stack name probably and with a 6 hours ttl? it could be reasonable for you?
JanDeDobbeleer: @lzecca78 the good news is that the cache timeout is easily configurable. So you can add a sane default for others to override.
kema-dev: Pulumi stacks can change over time as it is just a selection of the current environment we would like to deploy to. Running `pulumi stack select <stack name>` alters this stack, and this command can be used pretty often when working on multi-stack projects, making me think that caching is not the right solution there.

Quickly analyzing trace of `pulumi stack <args>` made me think that such commands are making HTTP requests and parses retrieved information, this could explain the high amount of time those commands take.

However, I found [there](https://github.com/pulumi/pulumi/blob/master/sdk/go/common/workspace/paths.go#L266) that currently selected stack is tracked locally, and [there](https://github.com/pulumi/pulumi/blob/master/sdk/go/common/workspace/workspace.go#L191) how this file's path is computed. This gave me a path like `$HOME/.pulumi/workspaces/<project>-<hash>-workspace.json` with `project name` value found in current directory as value of the `name` key in `Pulumi.{yaml:json}`, and `hash` sha1sum of absolute path of the related `Pulumi.{yaml:json}` with filename included. This file contains the current stack name as value of `stack` key in the JSON.

Parsing the appropriate file would be much faster, I think that we can avoid caching by doing so as the necessary computations are fairly trivial.
lzecca78: @kema-dev, great! Nice catch! 
IMHO the cache can still be a solution. Given that in those files `$HOME/.pulumi/workspaces/<project>-sha-workspace.json` only contains the stack name, given that this file exists only once you've selected the stack and given that this doesn't solve the `pulumi about` command, maybe  cache can be still adopted with the following logic: 
`key: <project-stack> `
`value : <pulumi-about-output> `
Where project is taken, as you said, from the `Pulumi.{yaml:json}`  file in the directory of the project from the key `name` and the stack from the file you documented above.
So following the above example, once you have selected the stack (that is something that you have to do in a way or another manually), you can cache once per `project-stack` (that should be pretty unique key) the about output avoiding to hit the content,  _often_ . 
The only question is, _how much this cache should last?_ 1 day? 1 week?  
This could be a mixed solution that takes somehow in consideration your suggestions but still takes as main solution the cache that  from my point of view is still relevant given the time that the command takes.
 


kema-dev: You are right, caching by stack seems a great idea !
Looking into [this](https://github.com/pulumi/pulumi/blob/master/sdk/go/common/workspace/creds.go#L169), it seems that credentials file could be handy. [Pulumi docs](https://www.pulumi.com/docs/concepts/state/) also suggests using `pulumi whoami`, might it be a local and fast command ? I might dig into this tomorrow !
lzecca78: hi @kema-dev  @JanDeDobbeleer ! 
Sorry for the delay but during the week is very hard to put hands on this. 
I've had heavily refactored pretty much everything accordingly with what we said in the conversation, is not finish yet the PR (in the happy path scenario I still have to update the doc accordingly with these changes)
When you have a chance, please give a look to what I did just to tell me what do you think

lzecca78: @kema-dev ,  I was trying to test locally but for reason that I am not able to understand 🤔 , even if putting to the `Enabled()`  `return true` function in the pulumi segment, I always get Pulumi(false) in `oh-my-posh debug` command, although I've enabled it by adding this map into the configuration:

```json
        {
          "background": "#316ce4",
          "foreground": "#ffffff",
          "leading_diamond": "\ue0b6",
          "style": "diamond",
          "template": "\uf308 {{ .Stack }}{{if .User }} :: {{ .User }}@{{ end }}{{ if .URL }}{{ .URL }}{{ end }}",
          "trailing_diamond": "\ue0b4",
          "type": "pulumi"
        },
```


kema-dev: @lzecca78 I am experiencing same behavior, I don't really know why :shrug: 

I could get a proper `Stack` name display within the segment with the suggested changes. Did not test the caching though
lzecca78:  @kema-dev , thanks for tips, I've slightly adjusted the code (see https://github.com/JanDeDobbeleer/oh-my-posh/pull/4650/commits/06d9da555a7db5ff04dab583bbc702a1503681f2) and fixed wrong behavior with cache when the cache wasn't hit, now everything seems working perfectly 🎉 !

lzecca78: Hi @JanDeDobbeleer , just ping to understand if there is still something that I need to improve or to be reviewed. Let me know so I can do changes accordingly to that. Thanks 🙏
JanDeDobbeleer: @lzecca78 I pushed my changes, can you validate this still works as designed?
lzecca78: @JanDeDobbeleer , I didn't have a lot of time, but I've tried testing it directly and is working partially. the `about` section unfortunately is not working 😞 , while in my previous implementation works. Another weird stuff happens when you do `oh-my-posh debug` that I see the pulumi segment `off` although is active and working fine 🤔 
JanDeDobbeleer: @lzecca78 did you change the properties to use the new naming? Because that could still be the issue. `oh-my-posh debug` should indicate the same as the prompt when in the same folder.
lzecca78: @JanDeDobbeleer , yes I did it, otherwise neither the stack name was visible 👍 
JanDeDobbeleer: @lzecca78 can you share the logs?
JanDeDobbeleer: Yes!
lzecca78: @JanDeDobbeleer i saw the log, but there isn't any reference to pulumi. Even the prompt:

```
Prompt:

╭─ zsh  pippo    feat/pippo-pluto ≡  ?1  0ms⠀                                                                            it@eu-west-1  example-staging :: posh    100   27,15:27 
╰─
```
is not the real one (i have the pulumi stack in the prompt, but I dunno why is not rendered in the log.
And in the segment list, pulumi is false

```

Segments:

ConsoleTitle(false) -   0 ms
Shell(true)         -   1 ms
Root(false)         -   0 ms
Path(true)          -   1 ms
Git(true)           -  35 ms
Executiontime(true) -   1 ms
Node(false)         -   3 ms
Python(false)       -   1 ms
Java(false)         -   3 ms
Dotnet(false)       -   3 ms
Go(false)           -   1 ms
Rust(false)         -   2 ms
Dart(false)         -   1 ms
Angular(false)      -   1 ms
Nx(false)           -   1 ms
Julia(false)        -   0 ms
Ruby(false)         -   2 ms
Azfunc(false)       -   2 ms
Aws(true)           -   0 ms
Kubectl(true)       -  32 ms
Pulumi(false)       -   0 ms
Os(true)            -   0 ms
Battery(true)       -  19 ms
Time(true)          -   0 ms
Text(true)          -   0 ms
Status(true)        -   0 ms
```


In the other logs lines, there is no any traces of the pulumi logs that should be present. ( I avoid to paste them here because they don't contain any meaningful information for debugging the pulumi segment)
JanDeDobbeleer: @lzecca78 I would really love the logs so I can have a look. If there's nothing relevant regarding pulumi, that could also indicate it's a version of oh-my-posh without that segment logic.
JanDeDobbeleer: @lzecca78 why is your version `Version: 19.8.2`? Did you build with the same version number? Because I'd expect `development` here as that's the default value.
lzecca78: @JanDeDobbeleer , probably i locally recompiled without changing the version, but it shouldn't affect the behavior, isn't it? 
JanDeDobbeleer: @lzecca78 but that's not possible, the version isn't part of the code. So if you didn't set it, I'm quite certain you're using a production release without that code.
lzecca78: i've did `go build -o oh-my-posh` inside the `src/` dir without changing the version tbh

JanDeDobbeleer: @lzecca78 yes, but them the output of `oh-my-posh version` needs to be `development` (also for the logs) and not `Version: 19.8.2`. So, you're not testing with that build is what I'm saying.

kema-dev: @lzecca78 I think that what Jan means is that, based on your `$PATH`, your `oh-my-posh` command is your globally installed binary, not the one you built using `go build`. Locally, running `oh-my-posh debug` on a binary built from your up-to-date PR prints `Pulumi(true)`, and `Version:` (no version). However, no `{{ .Stack }}` is printed in the prompt.
JanDeDobbeleer: @lzecca78 so this can be merged?
lzecca78: Maybe we should check if linting/testing is ok and ✅ , but on top of that, for me yes 👍 
kema-dev: I still get `False` for the segment, with relevant trace here:
```log
[TRACE] 18:54:40.297 shell.go:HasFilesInDir(Pulumi.json) - 11.65µs
[ERROR] 18:54:40.297 pulumi.go:Enabled:67 → no pulumi spec file found
```
lzecca78: > I still get `False` for the segment, with relevant trace here:
> ```log
> [TRACE] 18:54:40.297 shell.go:HasFilesInDir(Pulumi.json) - 11.65µs
> [ERROR] 18:54:40.297 pulumi.go:Enabled:67 → no pulumi spec file found
> ```

In my case using the right binary compiled the segment is set to true
kema-dev: > > I still get `False` for the segment, with relevant trace here:
> > ```
> > [TRACE] 18:54:40.297 shell.go:HasFilesInDir(Pulumi.json) - 11.65µs
> > [ERROR] 18:54:40.297 pulumi.go:Enabled:67 → no pulumi spec file found
> > ```
> 
> In my case using the right binary compiled the segment is set to true

Double my bad:
- `Pulumi(False)` was caused by not being in a "pulumi project dir", no `Pulumi.{yaml,json}` present
- Had no stack info because I had old values in my theme, I noticed you now have `fetch_stack` and `fetch_about` but was using `pulumi_user` and `pulumi_url`

Everything works fine ! Switching stack works, caching as needed.

LGTM !

And thank you / well done though ! :heart: 
lzecca78: @JanDeDobbeleer I've reworded the commits accordingly with the commitlint Github Workflow, now it could be merged I guess

Patch:
diff --git a/src/engine/segment.go b/src/engine/segment.go
index ebc9a928..dd263b72 100644
--- a/src/engine/segment.go
+++ b/src/engine/segment.go
@@ -202,6 +202,8 @@ const (
 	PLASTIC SegmentType = "plastic"
 	// Project version
 	PROJECT SegmentType = "project"
+	// PULUMI writes the pulumi user, store and stack
+	PULUMI SegmentType = "pulumi"
 	// PYTHON writes the virtual env name
 	PYTHON SegmentType = "python"
 	// QUASAR writes the QUASAR version and context
@@ -322,6 +324,7 @@ var Segments = map[SegmentType]func() SegmentWriter{
 	PHP:             func() SegmentWriter { return &segments.Php{} },
 	PLASTIC:         func() SegmentWriter { return &segments.Plastic{} },
 	PROJECT:         func() SegmentWriter { return &segments.Project{} },
+	PULUMI:          func() SegmentWriter { return &segments.Pulumi{} },
 	PYTHON:          func() SegmentWriter { return &segments.Python{} },
 	QUASAR:          func() SegmentWriter { return &segments.Quasar{} },
 	R:               func() SegmentWriter { return &segments.R{} },
diff --git a/src/segments/pulumi.go b/src/segments/pulumi.go
new file mode 100644
index 00000000..e514893d
--- /dev/null
+++ b/src/segments/pulumi.go
@@ -0,0 +1,220 @@
+package segments
+
+import (
+	"crypto/sha1"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"path/filepath"
+
+	"github.com/jandedobbeleer/oh-my-posh/src/platform"
+	"github.com/jandedobbeleer/oh-my-posh/src/properties"
+	"gopkg.in/yaml.v3"
+)
+
+const (
+	FetchStack properties.Property = "fetch_stack"
+	FetchAbout properties.Property = "fetch_about"
+
+	JSON string = "json"
+	YAML string = "yaml"
+
+	pulumiJSON string = "Pulumi.json"
+	pulumiYAML string = "Pulumi.yaml"
+)
+
+type Pulumi struct {
+	props properties.Properties
+	env   platform.Environment
+
+	Stack string
+	Name  string
+
+	workspaceSHA1 string
+
+	backend
+}
+
+type backend struct {
+	URL  string `json:"url"`
+	User string `json:"user"`
+}
+
+type pulumiFileSpec struct {
+	Name string `yaml:"name" json:"name"`
+}
+
+type pulumiWorkSpaceFileSpec struct {
+	Stack string `json:"stack"`
+}
+
+func (p *Pulumi) Template() string {
+	return "\U000f0d46 {{ .Stack }}{{if .User }} :: {{ .User }}@{{ end }}{{ if .URL }}{{ .URL }}{{ end }}"
+}
+
+func (p *Pulumi) Init(props properties.Properties, env platform.Environment) {
+	p.props = props
+	p.env = env
+}
+
+func (p *Pulumi) Enabled() bool {
+	if !p.env.HasCommand("pulumi") {
+		return false
+	}
+
+	err := p.getProjectName()
+	if err != nil {
+		p.env.Error(err)
+		return false
+	}
+
+	if p.props.GetBool(FetchStack, false) {
+		p.getPulumiStackName()
+	}
+
+	if p.props.GetBool(FetchAbout, false) {
+		p.getPulumiAbout()
+	}
+
+	return true
+}
+
+func (p *Pulumi) getPulumiStackName() {
+	if len(p.Name) == 0 || len(p.workspaceSHA1) == 0 {
+		p.env.Debug("pulumi project name or workspace sha1 is empty")
+		return
+	}
+
+	stackNameFile := p.Name + "-" + p.workspaceSHA1 + "-" + "workspace.json"
+
+	homedir := p.env.Home()
+
+	workspaceCacheDir := filepath.Join(homedir, ".pulumi", "workspaces")
+	if !p.env.HasFolder(workspaceCacheDir) || !p.env.HasFilesInDir(workspaceCacheDir, stackNameFile) {
+		return
+	}
+
+	workspaceCacheFile := filepath.Join(workspaceCacheDir, stackNameFile)
+	workspaceCacheFileContent := p.env.FileContent(workspaceCacheFile)
+
+	var pulumiWorkspaceSpec pulumiWorkSpaceFileSpec
+	err := json.Unmarshal([]byte(workspaceCacheFileContent), &pulumiWorkspaceSpec)
+	if err != nil {
+		p.env.Error(fmt.Errorf("pulumi workspace file decode error"))
+		return
+	}
+
+	p.env.DebugF("pulumi stack name: %s", pulumiWorkspaceSpec.Stack)
+	p.Stack = pulumiWorkspaceSpec.Stack
+}
+
+func (p *Pulumi) getProjectName() error {
+	var kind, fileName string
+	for _, file := range []string{pulumiYAML, pulumiJSON} {
+		if p.env.HasFiles(file) {
+			fileName = file
+			kind = filepath.Ext(file)[1:]
+		}
+	}
+
+	if len(kind) == 0 {
+		return fmt.Errorf("no pulumi spec file found")
+	}
+
+	var pulumiFileSpec pulumiFileSpec
+	var err error
+
+	pulumiFile := p.env.FileContent(fileName)
+
+	switch kind {
+	case YAML:
+		err = yaml.Unmarshal([]byte(pulumiFile), &pulumiFileSpec)
+	case JSON:
+		err = json.Unmarshal([]byte(pulumiFile), &pulumiFileSpec)
+	default:
+		err = fmt.Errorf("unknown pulumi spec file format")
+	}
+
+	if err != nil {
+		p.env.Error(err)
+		return nil
+	}
+
+	p.Name = pulumiFileSpec.Name
+
+	sha1HexString := func(value string) string {
+		h := sha1.New()
+
+		_, err := h.Write([]byte(value))
+		if err != nil {
+			p.env.Error(err)
+			return ""
+		}
+
+		return hex.EncodeToString(h.Sum(nil))
+	}
+
+	p.workspaceSHA1 = sha1HexString(p.env.Pwd() + "/" + fileName)
+
+	return nil
+}
+
+func (p *Pulumi) getPulumiAbout() {
+	if len(p.Stack) == 0 {
+		p.env.Error(fmt.Errorf("pulumi stack name is empty, use `fetch_stack` property to enable stack fetching"))
+		return
+	}
+
+	cacheKey := "pulumi-" + p.Name + "-" + p.Stack + "-" + p.workspaceSHA1 + "-about"
+
+	getAboutCache := func(key string) (*backend, error) {
+		aboutBackend, OK := p.env.Cache().Get(key)
+		if (!OK || len(aboutBackend) == 0) || (OK && len(aboutBackend) == 0) {
+			return nil, fmt.Errorf("no data in cache")
+		}
+
+		var backend *backend
+		err := json.Unmarshal([]byte(aboutBackend), &backend)
+		if err != nil {
+			p.env.DebugF("unable to decode about cache: %s", aboutBackend)
+			p.env.Error(fmt.Errorf("pulling about cache decode error"))
+			return nil, err
+		}
+
+		return backend, nil
+	}
+
+	aboutBackend, err := getAboutCache(cacheKey)
+	if err == nil {
+		p.backend = *aboutBackend
+		return
+	}
+
+	aboutOutput, err := p.env.RunCommand("pulumi", "about", "--json")
+
+	if err != nil {
+		p.env.Error(fmt.Errorf("unable to get pulumi about output"))
+		return
+	}
+
+	var about struct {
+		Backend *backend `json:"backend"`
+	}
+
+	err = json.Unmarshal([]byte(aboutOutput), &about)
+	if err != nil {
+		p.env.Error(fmt.Errorf("pulumi about output decode error"))
+		return
+	}
+
+	if about.Backend == nil {
+		p.env.Debug("pulumi about backend is not set")
+		return
+	}
+
+	p.backend = *about.Backend
+
+	cacheTimeout := p.props.GetInt(properties.CacheTimeout, 43800)
+	jso, _ := json.Marshal(about.Backend)
+	p.env.Cache().Set(cacheKey, string(jso), cacheTimeout)
+}
diff --git a/themes/schema.json b/themes/schema.json
index 3e4e91cf..1582a047 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -295,6 +295,7 @@
             "php",
             "plastic",
             "project",
+            "pulumi",
             "root",
             "ruby",
             "rust",
@@ -1957,6 +1958,19 @@
             }
           }
         },
+        {
+          "if": {
+            "properties": {
+              "type": {
+                "const": "pulumi"
+              }
+            }
+          },
+          "then": {
+            "title": "Pulumi Segment",
+            "description": "https://ohmyposh.dev/docs/segments/pulumi"
+          }
+        },
         {
           "if": {
             "properties": {
diff --git a/website/docs/segments/pulumi.mdx b/website/docs/segments/pulumi.mdx
new file mode 100644
index 00000000..0b1177dc
--- /dev/null
+++ b/website/docs/segments/pulumi.mdx
@@ -0,0 +1,56 @@
+---
+id: pulumi
+title: Pulumi
+sidebar_label: Pulumi
+---
+
+## What
+
+Display the currently active pulumi logged-in user, url and stack.
+
+:::caution
+This requires a pulumi binary in your PATH and will only show in directories that contain a `Pulumi.yaml` file.
+:::
+
+## Sample Configuration
+
+import Config from "@site/src/components/Config.js";
+
+<Config
+  data={{
+    type: "pulumi",
+    style: "diamond",
+    powerline_symbol: "\uE0CF",
+    foreground: "#ffffff",
+    background: "#662d91",
+    template:
+      "{{ .Stack }}{{if .User }} :: {{ .User }}@{{ end }}{{ if .URL }}{{ .URL }}{{ end }}",
+  }}
+/>
+
+## Properties
+
+| Name          | Type      | Default | Description                                                                        |
+| ------------- | --------- | ------- | ---------------------------------------------------------------------------------- |
+| `fetch_stack` | `boolean` | `false` | fetch the current stack name                                                       |
+| `fetch_about` | `boolean` | `false` | fetch the URL and user for the current stask. Requires `fetch_stack` set to `true` |
+
+## Template ([info][templates])
+
+:::note default template
+
+```template
+{{ .Stack }}{{if .User }} :: {{ .User }}@{{ end }}{{ if .URL }}{{ .URL }}{{ end }}
+```
+
+:::
+
+### Properties
+
+| Name     | Type     | Description                                        |
+| -------- | -------- | -------------------------------------------------- |
+| `.Stack` | `string` | the current stack name                             |
+| `.User`  | `string` | is the current logged in user                      |
+| `.Url`   | `string` | the URL of the state where pulumi stores resources |
+
+[templates]: /docs/configuration/templates
diff --git a/website/sidebars.js b/website/sidebars.js
index a74ee249..0aba4195 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -104,6 +104,7 @@ module.exports = {
         "segments/php",
         "segments/plastic",
         "segments/project",
+        "segments/pulumi",
         "segments/python",
         "segments/quasar",
         "segments/r",


Test patch:
diff --git a/src/segments/pulumi_test.go b/src/segments/pulumi_test.go
new file mode 100644
index 00000000..bd277283
--- /dev/null
+++ b/src/segments/pulumi_test.go
@@ -0,0 +1,234 @@
+package segments
+
+import (
+	"errors"
+	"path/filepath"
+	"testing"
+
+	"github.com/jandedobbeleer/oh-my-posh/src/mock"
+	"github.com/jandedobbeleer/oh-my-posh/src/properties"
+	"github.com/stretchr/testify/assert"
+	mock2 "github.com/stretchr/testify/mock"
+)
+
+func TestPulumi(t *testing.T) {
+	cases := []struct {
+		Case       string
+		YAMLConfig string
+		JSONConfig string
+
+		HasCommand bool
+
+		FetchStack bool
+		Stack      string
+		StackError error
+
+		HasWorkspaceFolder bool
+		WorkSpaceFile      string
+
+		FetchAbout bool
+		About      string
+		AboutError error
+		AboutCache string
+
+		ExpectedString  string
+		ExpectedEnabled bool
+	}{
+		{
+			Case:            "no pulumi command",
+			ExpectedEnabled: false,
+			HasCommand:      false,
+		},
+		{
+			Case:            "pulumi command is present, but no pulumi file",
+			ExpectedEnabled: false,
+			HasCommand:      true,
+		},
+		{
+			Case:            "pulumi file YAML is present",
+			ExpectedString:  "\U000f0d46",
+			ExpectedEnabled: true,
+			HasCommand:      true,
+			YAMLConfig: `
+name: oh-my-posh
+runtime: golang
+description: A Console App
+`,
+		},
+		{
+			Case:            "pulumi file JSON is present",
+			ExpectedString:  "\U000f0d46",
+			ExpectedEnabled: true,
+			HasCommand:      true,
+			JSONConfig:      `{ "name": "oh-my-posh" }`,
+		},
+		{
+			Case:               "no stack present",
+			ExpectedString:     "\U000f0d46 1337",
+			ExpectedEnabled:    true,
+			HasCommand:         true,
+			HasWorkspaceFolder: true,
+			FetchStack:         true,
+			JSONConfig:         `{ "name": "oh-my-posh" }`,
+			WorkSpaceFile:      `{ "stack": "1337" }`,
+		},
+		{
+			Case:               "pulumi stack",
+			ExpectedString:     "\U000f0d46 1337",
+			ExpectedEnabled:    true,
+			HasCommand:         true,
+			HasWorkspaceFolder: true,
+			FetchStack:         true,
+			JSONConfig:         `{ "name": "oh-my-posh" }`,
+			WorkSpaceFile:      `{ "stack": "1337" }`,
+		},
+		{
+			Case:               "pulumi URL",
+			ExpectedString:     "\U000f0d46 1337 :: posh-user@s3://test-pulumi-state-test",
+			ExpectedEnabled:    true,
+			HasCommand:         true,
+			HasWorkspaceFolder: true,
+			FetchStack:         true,
+			FetchAbout:         true,
+			JSONConfig:         `{ "name": "oh-my-posh" }`,
+			WorkSpaceFile:      `{ "stack": "1337" }`,
+			About:              `{ "backend": { "url": "s3://test-pulumi-state-test", "user":"posh-user" } }`,
+		},
+		{
+			Case:               "pulumi URL - cache",
+			ExpectedString:     "\U000f0d46 1337 :: posh-user@s3://test-pulumi-state-test",
+			ExpectedEnabled:    true,
+			HasCommand:         true,
+			HasWorkspaceFolder: true,
+			FetchStack:         true,
+			FetchAbout:         true,
+			JSONConfig:         `{ "name": "oh-my-posh" }`,
+			WorkSpaceFile:      `{ "stack": "1337" }`,
+			AboutCache:         `{ "url": "s3://test-pulumi-state-test", "user":"posh-user" }`,
+		},
+		// Error flows
+		{
+			Case:            "pulumi file JSON error",
+			ExpectedString:  "\U000f0d46",
+			ExpectedEnabled: true,
+			FetchStack:      true,
+			HasCommand:      true,
+			JSONConfig:      `{`,
+		},
+		{
+			Case:               "pulumi workspace file JSON error",
+			ExpectedString:     "\U000f0d46",
+			ExpectedEnabled:    true,
+			FetchStack:         true,
+			HasCommand:         true,
+			JSONConfig:         `{ "name": "oh-my-posh" }`,
+			WorkSpaceFile:      `{`,
+			HasWorkspaceFolder: true,
+		},
+		{
+			Case:            "pulumi URL, no fetch_stack set",
+			ExpectedString:  "\U000f0d46",
+			ExpectedEnabled: true,
+			HasCommand:      true,
+			FetchAbout:      true,
+			JSONConfig:      `{ "name": "oh-my-posh" }`,
+		},
+		{
+			Case:               "pulumi URL - cache error",
+			ExpectedString:     "\U000f0d46 1337 :: posh-user@s3://test-pulumi-state-test-output",
+			ExpectedEnabled:    true,
+			HasCommand:         true,
+			HasWorkspaceFolder: true,
+			FetchStack:         true,
+			FetchAbout:         true,
+			JSONConfig:         `{ "name": "oh-my-posh" }`,
+			WorkSpaceFile:      `{ "stack": "1337" }`,
+			AboutCache:         `{`,
+			About:              `{ "backend": { "url": "s3://test-pulumi-state-test-output", "user":"posh-user" } }`,
+		},
+		{
+			Case:               "pulumi URL - about error",
+			ExpectedString:     "\U000f0d46 1337",
+			ExpectedEnabled:    true,
+			HasCommand:         true,
+			HasWorkspaceFolder: true,
+			FetchStack:         true,
+			FetchAbout:         true,
+			JSONConfig:         `{ "name": "oh-my-posh" }`,
+			WorkSpaceFile:      `{ "stack": "1337" }`,
+			AboutError:         errors.New("error"),
+		},
+		{
+			Case:               "pulumi URL - about decode error",
+			ExpectedString:     "\U000f0d46 1337",
+			ExpectedEnabled:    true,
+			HasCommand:         true,
+			HasWorkspaceFolder: true,
+			FetchStack:         true,
+			FetchAbout:         true,
+			JSONConfig:         `{ "name": "oh-my-posh" }`,
+			WorkSpaceFile:      `{ "stack": "1337" }`,
+			About:              `{`,
+		},
+		{
+			Case:               "pulumi URL - about backend is nil",
+			ExpectedString:     "\U000f0d46 1337",
+			ExpectedEnabled:    true,
+			HasCommand:         true,
+			HasWorkspaceFolder: true,
+			FetchStack:         true,
+			FetchAbout:         true,
+			JSONConfig:         `{ "name": "oh-my-posh" }`,
+			WorkSpaceFile:      `{ "stack": "1337" }`,
+			About:              `{}`,
+		},
+	}
+
+	for _, tc := range cases {
+		env := new(mock.MockedEnvironment)
+
+		env.On("HasCommand", "pulumi").Return(tc.HasCommand)
+		env.On("RunCommand", "pulumi", []string{"stack", "ls", "--json"}).Return(tc.Stack, tc.StackError)
+		env.On("RunCommand", "pulumi", []string{"about", "--json"}).Return(tc.About, tc.AboutError)
+
+		env.On("Pwd").Return("/home/foobar/Work/oh-my-posh/pulumi/projects/awesome-project")
+		env.On("Home").Return(filepath.Clean("/home/foobar"))
+		env.On("Error", mock2.Anything)
+		env.On("Debug", mock2.Anything)
+		env.On("DebugF", mock2.Anything, mock2.Anything)
+
+		env.On("HasFiles", pulumiYAML).Return(len(tc.YAMLConfig) > 0)
+		env.On("FileContent", pulumiYAML).Return(tc.YAMLConfig, nil)
+
+		env.On("HasFiles", pulumiJSON).Return(len(tc.JSONConfig) > 0)
+		env.On("FileContent", pulumiJSON).Return(tc.JSONConfig, nil)
+
+		env.On("HasFolder", filepath.Clean("/home/foobar/.pulumi/workspaces")).Return(tc.HasWorkspaceFolder)
+		workspaceFile := "oh-my-posh-c62b7b6786c5c5a85896576e46a25d7c9f888e92-workspace.json"
+		env.On("HasFilesInDir", filepath.Clean("/home/foobar/.pulumi/workspaces"), workspaceFile).Return(len(tc.WorkSpaceFile) > 0)
+		env.On("FileContent", filepath.Clean("/home/foobar/.pulumi/workspaces/"+workspaceFile)).Return(tc.WorkSpaceFile, nil)
+
+		cache := &mock.MockedCache{}
+		cache.On("Get", "pulumi-oh-my-posh-1337-c62b7b6786c5c5a85896576e46a25d7c9f888e92-about").Return(tc.AboutCache, len(tc.AboutCache) > 0)
+		cache.On("Set", mock2.Anything, mock2.Anything, mock2.Anything)
+
+		env.On("Cache").Return(cache)
+
+		pulumi := &Pulumi{
+			env: env,
+			props: properties.Map{
+				FetchStack: tc.FetchStack,
+				FetchAbout: tc.FetchAbout,
+			},
+		}
+
+		assert.Equal(t, tc.ExpectedEnabled, pulumi.Enabled(), tc.Case)
+
+		if !tc.ExpectedEnabled {
+			continue
+		}
+
+		var got = renderTemplate(env, pulumi.Template(), pulumi)
+		assert.Equal(t, tc.ExpectedString, got, tc.Case)
+	}
+}