From e7e5e12da4275addd628b8372264feecb49b6383 Mon Sep 17 00:00:00 2001 From: rikohomeless <163776849+rikohomeless@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:51:03 +0000 Subject: [PATCH 1/2] fix(shell): surface clone source in workspace context --- .../api/src/services/terminal-sessions.ts | 5 +- packages/api/tests/terminal-sessions.test.ts | 2 +- .../app/src/docker-git/open-project-ssh.ts | 9 +- .../src/web/app-ready-controller-context.ts | 4 +- .../src/web/app-ready-ssh-link-terminal.ts | 4 +- .../tests/docker-git/open-project-ssh.test.ts | 4 +- packages/terminal/src/core/index.ts | 1 + .../src/core/project-terminal-label.ts | 134 ++++++++++++++++++ .../tests/core/project-terminal-label.test.ts | 31 ++++ scripts/e2e/clone-auto-open-ssh.sh | 4 +- 10 files changed, 185 insertions(+), 13 deletions(-) create mode 100644 packages/terminal/src/core/project-terminal-label.ts create mode 100644 packages/terminal/tests/core/project-terminal-label.test.ts diff --git a/packages/api/src/services/terminal-sessions.ts b/packages/api/src/services/terminal-sessions.ts index 800df89b..e3e26beb 100644 --- a/packages/api/src/services/terminal-sessions.ts +++ b/packages/api/src/services/terminal-sessions.ts @@ -27,6 +27,7 @@ import { appendTerminalOutput, createTerminalImagePastePlan, emptyTerminalOutputBuffer, + projectTerminalLabel, renderTerminalOutputBuffer, terminalImagePasteDirectory, type TerminalImagePastePayload, @@ -1399,7 +1400,7 @@ export const createTerminalSession = ( const session = yield* _(registerRecord( resolvedProjectId, project.projectKey, - project.displayName, + projectTerminalLabel(project), prepared, projectItem.containerName, projectItem.targetDir, @@ -1421,7 +1422,7 @@ export const createTerminalSession = ( const session = yield* _(registerRecord( resolvedProjectId, startedProject.projectKey, - startedProject.displayName, + projectTerminalLabel(startedProject), prepared, reachableProjectItem.containerName, reachableProjectItem.targetDir, diff --git a/packages/api/tests/terminal-sessions.test.ts b/packages/api/tests/terminal-sessions.test.ts index 42ad86d2..00083da2 100644 --- a/packages/api/tests/terminal-sessions.test.ts +++ b/packages/api/tests/terminal-sessions.test.ts @@ -414,7 +414,7 @@ describe("terminal sessions service", () => { status: "ready" }) await expect(runTestEffect(lookupTerminalSessionById(second.session.id))).resolves.toMatchObject({ - projectDisplayName: displayName, + projectDisplayName: "org/repo | issue #7 (https://github.com/org/repo/issues/7) | container dg-repo-issue-7", projectKey, session: { id: second.session.id, diff --git a/packages/app/src/docker-git/open-project-ssh.ts b/packages/app/src/docker-git/open-project-ssh.ts index 2881e6a8..174c55f4 100644 --- a/packages/app/src/docker-git/open-project-ssh.ts +++ b/packages/app/src/docker-git/open-project-ssh.ts @@ -1,6 +1,7 @@ import type { PlatformError } from "@effect/platform/Error" import * as FileSystem from "@effect/platform/FileSystem" import * as Path from "@effect/platform/Path" +import { projectTerminalLabel } from "@prover-coder-ai/docker-git-terminal/core" import { Duration, Effect } from "effect" import { createProjectTerminalSession, upProject } from "./api-client.js" @@ -156,7 +157,7 @@ const resolveHostSshLaunchSpec = ( const writeProjectSshHeader = (item: ProjectItem): Effect.Effect => Effect.sync(() => { - writeToTerminal(`\n[docker-git] SSH terminal: ${item.displayName}\n`) + writeToTerminal(`\n[docker-git] SSH terminal: ${projectTerminalLabel(item)}\n`) writeToTerminal(`[docker-git] ${item.sshCommand}\n\n`) }) @@ -203,9 +204,9 @@ export const openResolvedProjectSshWithUpEffect = ( ) => Effect.gen(function*(_) { const writeProgress = deps.writeProgress ?? writeProjectOpenProgress - yield* _(writeProgress(`Starting project before SSH: ${item.displayName}`)) + yield* _(writeProgress(`Starting project before SSH: ${projectTerminalLabel(item)}`)) const refreshedItem = yield* _(deps.upProject(item.projectDir)) - yield* _(writeProgress(`Opening SSH terminal: ${(refreshedItem ?? item).displayName}`)) + yield* _(writeProgress(`Opening SSH terminal: ${projectTerminalLabel(refreshedItem ?? item)}`)) yield* _(deps.openProjectSsh(refreshedItem ?? item)) }) @@ -241,7 +242,7 @@ export const openResolvedProjectSshViaController = (item: ProjectItem) => createSession: (projectId) => createProjectTerminalSession(projectId), attach: (project, session) => attachTerminalSession({ - header: `SSH terminal: ${project.displayName}`, + header: `SSH terminal: ${projectTerminalLabel(project)}`, session, websocketPath: `/projects/${encodeURIComponent(project.projectDir)}/terminal-sessions/${ encodeURIComponent(session.id) diff --git a/packages/app/src/web/app-ready-controller-context.ts b/packages/app/src/web/app-ready-controller-context.ts index 74435a56..a968239f 100644 --- a/packages/app/src/web/app-ready-controller-context.ts +++ b/packages/app/src/web/app-ready-controller-context.ts @@ -1,3 +1,5 @@ +import { projectTerminalLabel } from "@prover-coder-ai/docker-git-terminal/core" + import type { DashboardData } from "./api.js" import { createActionContext } from "./app-ready-actions.js" import type { ReadyState } from "./app-ready-hooks.js" @@ -23,7 +25,7 @@ export const createReadyActionContext = ( refreshDashboard, selectedProjectId: state.selectedProjectId, selectedProjectKey: selectedProjectSummary?.projectKey ?? null, - selectedProjectName: selectedProjectSummary?.displayName ?? null, + selectedProjectName: selectedProjectSummary === undefined ? null : projectTerminalLabel(selectedProjectSummary), setActionPrompt: state.setActionPrompt, setActiveScreen: state.setActiveScreen, setAuthSnapshot: state.setAuthSnapshot, diff --git a/packages/app/src/web/app-ready-ssh-link-terminal.ts b/packages/app/src/web/app-ready-ssh-link-terminal.ts index 4d2099f4..cf5790a8 100644 --- a/packages/app/src/web/app-ready-ssh-link-terminal.ts +++ b/packages/app/src/web/app-ready-ssh-link-terminal.ts @@ -1,3 +1,5 @@ +import { projectTerminalLabel } from "@prover-coder-ai/docker-git-terminal/core" + import type { BrowserActionContext } from "./actions-shared.js" import type { TerminalSession } from "./api-types.js" import type { DashboardProject } from "./app-ready-ssh-link-core.js" @@ -135,7 +137,7 @@ const buildProjectTerminalSession = ( buildProjectActiveTerminalSession({ onExit: args.actionContext.reloadDashboard, onReady: args.actionContext.reloadDashboard, - projectDisplayName: project.displayName, + projectDisplayName: projectTerminalLabel(project), projectId: project.id, projectKey: project.projectKey, session diff --git a/packages/app/tests/docker-git/open-project-ssh.test.ts b/packages/app/tests/docker-git/open-project-ssh.test.ts index 29a3c413..c31cff4d 100644 --- a/packages/app/tests/docker-git/open-project-ssh.test.ts +++ b/packages/app/tests/docker-git/open-project-ssh.test.ts @@ -69,9 +69,9 @@ describe("openResolvedProjectSshWithUpEffect", () => { }) const events = yield* _(captureOpenResolvedProjectSshWithUpEvents(item)) expect(events).toEqual([ - "progress:Starting project before SSH: org/repo", + "progress:Starting project before SSH: org/repo | source https://github.com/org/repo.git | container dg-repo", "up:/controller/org/repo/issue-9", - "progress:Opening SSH terminal: org/repo", + "progress:Opening SSH terminal: org/repo | source https://github.com/org/repo.git | container dg-repo", "open:ssh -p 2299 dev@127.0.0.1" ]) })) diff --git a/packages/terminal/src/core/index.ts b/packages/terminal/src/core/index.ts index cf242a9d..28678e56 100644 --- a/packages/terminal/src/core/index.ts +++ b/packages/terminal/src/core/index.ts @@ -1,2 +1,3 @@ export * from "./image-paste.js" export * from "./output-buffer.js" +export * from "./project-terminal-label.js" diff --git a/packages/terminal/src/core/project-terminal-label.ts b/packages/terminal/src/core/project-terminal-label.ts new file mode 100644 index 00000000..0b0f126d --- /dev/null +++ b/packages/terminal/src/core/project-terminal-label.ts @@ -0,0 +1,134 @@ +export type ProjectTerminalLabelInput = { + readonly containerName?: string | undefined + readonly displayName: string + readonly repoRef: string + readonly repoUrl: string +} + +const issueRefPattern = /^issue-(\d+)$/u +const githubPullRefPattern = /^refs\/pull\/(\d+)\/head$/u +const gitlabMergeRequestRefPattern = /^refs\/merge-requests\/(\d+)\/head$/u + +const stripGitSuffix = (value: string): string => value.endsWith(".git") ? value.slice(0, -4) : value + +const readPathPart = (value: string | undefined): string | null => { + const trimmed = value?.trim() ?? "" + return trimmed.length > 0 ? trimmed : null +} + +const splitGitHubRemotePath = (repoUrl: string): ReadonlyArray | null => { + const trimmed = repoUrl.trim() + const httpsPrefix = "https://github.com/" + const sshUrlPrefix = "ssh://git@github.com/" + const sshScpPrefix = "git@github.com:" + if (trimmed.startsWith(httpsPrefix)) { + return trimmed.slice(httpsPrefix.length).split("/").filter((part) => part.length > 0) + } + if (trimmed.startsWith(sshUrlPrefix)) { + return trimmed.slice(sshUrlPrefix.length).split("/").filter((part) => part.length > 0) + } + if (trimmed.startsWith(sshScpPrefix)) { + return trimmed.slice(sshScpPrefix.length).split("/").filter((part) => part.length > 0) + } + return null +} + +const githubRepositoryPath = (repoUrl: string): string | null => { + const parts = splitGitHubRemotePath(repoUrl) + const owner = readPathPart(parts?.[0]) + const repoRaw = readPathPart(parts?.[1]) + if (owner === null || repoRaw === null) { + return null + } + return `${owner}/${stripGitSuffix(repoRaw)}` +} + +const sourceUrlForContext = (repoUrl: string, path: string): string | null => { + const repoPath = githubRepositoryPath(repoUrl) + return repoPath === null ? null : `https://github.com/${repoPath}/${path}` +} + +const renderIssueContext = (repoUrl: string, issueId: string): string => { + const issueUrl = sourceUrlForContext(repoUrl, `issues/${issueId}`) + return issueUrl === null ? `issue #${issueId}` : `issue #${issueId} (${issueUrl})` +} + +const renderPullRequestContext = (repoUrl: string, pullRequestId: string): string => { + const pullRequestUrl = sourceUrlForContext(repoUrl, `pull/${pullRequestId}`) + return pullRequestUrl === null ? `PR #${pullRequestId}` : `PR #${pullRequestId} (${pullRequestUrl})` +} + +const renderMergeRequestContext = (mergeRequestId: string): string => `MR #${mergeRequestId}` + +const renderSourceContext = (repoUrl: string, repoRef: string): string => { + const trimmedRef = repoRef.trim() + return trimmedRef.length === 0 || trimmedRef === "main" + ? `source ${repoUrl.trim()}` + : `source ${repoUrl.trim()} (${trimmedRef})` +} + +const renderWorkspaceContext = ( + repoUrl: string, + repoRef: string +): string => { + const issueMatch = issueRefPattern.exec(repoRef) + if (issueMatch !== null) { + const issueId = issueMatch[1] + return issueId === undefined ? renderSourceContext(repoUrl, repoRef) : renderIssueContext(repoUrl, issueId) + } + const pullMatch = githubPullRefPattern.exec(repoRef) + if (pullMatch !== null) { + const pullRequestId = pullMatch[1] + return pullRequestId === undefined + ? renderSourceContext(repoUrl, repoRef) + : renderPullRequestContext(repoUrl, pullRequestId) + } + const mergeRequestMatch = gitlabMergeRequestRefPattern.exec(repoRef) + if (mergeRequestMatch !== null) { + const mergeRequestId = mergeRequestMatch[1] + return mergeRequestId === undefined + ? renderSourceContext(repoUrl, repoRef) + : renderMergeRequestContext(mergeRequestId) + } + return renderSourceContext(repoUrl, repoRef) +} + +const appendNonEmpty = (parts: ReadonlyArray, value: string): ReadonlyArray => { + const trimmed = value.trim() + return trimmed.length === 0 ? parts : [...parts, trimmed] +} + +/** + * Builds the terminal-facing project label with source workspace context. + * + * @param project - Project identity returned by the docker-git API. + * @returns A deterministic label for SSH terminal headers and ready messages. + * + * @pure true + * @effect none + * @invariant issue refs include an issue marker; PR refs include a PR marker; labels never omit displayName. + * @precondition project.displayName identifies the repository or fallback project label. + * @postcondition result contains displayName, workspace source context, and non-empty containerName when present. + * @complexity O(n) where n = |repoUrl| + |repoRef| + * @throws Never + */ +// CHANGE: surface clone-source context in SSH terminal labels +// WHY: terminal headers must identify issue/PR source and container instead of only the repo path +// QUOTE(ТЗ): "надо писать какой Issues какой PR вообещ что за конетейнер" +// REF: issue-370 +// SOURCE: n/a +// FORMAT THEOREM: forall p: label(p) contains displayName(p) and context(repoUrl(p), repoRef(p)) +// PURITY: CORE +// EFFECT: none +// INVARIANT: issue-* -> issue context; refs/pull/*/head -> PR context; containerName is preserved when non-empty +// COMPLEXITY: O(n) +export const projectTerminalLabel = (project: ProjectTerminalLabelInput): string => { + const displayName = project.displayName.trim() + const baseName = displayName.length === 0 ? project.repoUrl.trim() : displayName + const withContext = appendNonEmpty([baseName], renderWorkspaceContext(project.repoUrl, project.repoRef)) + const containerName = project.containerName?.trim() ?? "" + const withContainer = containerName.length === 0 + ? withContext + : appendNonEmpty(withContext, `container ${containerName}`) + return withContainer.join(" | ") +} diff --git a/packages/terminal/tests/core/project-terminal-label.test.ts b/packages/terminal/tests/core/project-terminal-label.test.ts new file mode 100644 index 00000000..0b6da1bd --- /dev/null +++ b/packages/terminal/tests/core/project-terminal-label.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest" + +import { projectTerminalLabel } from "../../src/core/project-terminal-label.js" + +describe("projectTerminalLabel", () => { + it("renders GitHub issue source context and container identity", () => { + expect(projectTerminalLabel({ + containerName: "dg-repo-issue-7", + displayName: "org/repo", + repoRef: "issue-7", + repoUrl: "https://github.com/org/repo.git" + })).toBe("org/repo | issue #7 (https://github.com/org/repo/issues/7) | container dg-repo-issue-7") + }) + + it("renders GitHub pull request source context from pull refs", () => { + expect(projectTerminalLabel({ + containerName: "dg-repo-pr-42", + displayName: "org/repo", + repoRef: "refs/pull/42/head", + repoUrl: "git@github.com:org/repo.git" + })).toBe("org/repo | PR #42 (https://github.com/org/repo/pull/42) | container dg-repo-pr-42") + }) + + it("renders repository source context for ordinary refs", () => { + expect(projectTerminalLabel({ + displayName: "org/repo", + repoRef: "feature-x", + repoUrl: "https://github.com/org/repo.git" + })).toBe("org/repo | source https://github.com/org/repo.git (feature-x)") + }) +}) diff --git a/scripts/e2e/clone-auto-open-ssh.sh b/scripts/e2e/clone-auto-open-ssh.sh index d18df1dd..c73dc9aa 100755 --- a/scripts/e2e/clone-auto-open-ssh.sh +++ b/scripts/e2e/clone-auto-open-ssh.sh @@ -246,8 +246,8 @@ fi grep -Fq -- "Project created: octocat/hello-world" "$CLONE_LOG" \ || fail "expected clone log to confirm project creation" -grep -Fq -- "SSH terminal: octocat/hello-world" "$CLONE_LOG" \ - || fail "expected clone log to show SSH auto-open header" +grep -Fq -- "SSH terminal: octocat/hello-world | issue #1 (https://github.com/octocat/Hello-World/issues/1) | container $CONTAINER_NAME" "$CLONE_LOG" \ + || fail "expected clone log to show SSH auto-open header with issue URL and container" [[ -f "$SSH_INVOCATION_LOG" ]] || fail "expected ssh wrapper to be invoked" grep -Fq -- "<-tt>" "$SSH_INVOCATION_LOG" || fail "expected ssh to request a tty" From d02ecf8c3ce34d1d7db982e91dc39eef03263a64 Mon Sep 17 00:00:00 2001 From: rikohomeless <163776849+rikohomeless@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:27:09 +0000 Subject: [PATCH 2/2] test(core): cover terminal label invariants --- .../src/core/project-terminal-label.ts | 37 +++-- .../tests/core/project-terminal-label.test.ts | 133 ++++++++++++++++++ 2 files changed, 151 insertions(+), 19 deletions(-) diff --git a/packages/terminal/src/core/project-terminal-label.ts b/packages/terminal/src/core/project-terminal-label.ts index 0b0f126d..95597917 100644 --- a/packages/terminal/src/core/project-terminal-label.ts +++ b/packages/terminal/src/core/project-terminal-label.ts @@ -5,9 +5,7 @@ export type ProjectTerminalLabelInput = { readonly repoUrl: string } -const issueRefPattern = /^issue-(\d+)$/u -const githubPullRefPattern = /^refs\/pull\/(\d+)\/head$/u -const gitlabMergeRequestRefPattern = /^refs\/merge-requests\/(\d+)\/head$/u +const decimalDigitsPattern = /^\d+$/u const stripGitSuffix = (value: string): string => value.endsWith(".git") ? value.slice(0, -4) : value @@ -67,28 +65,29 @@ const renderSourceContext = (repoUrl: string, repoRef: string): string => { : `source ${repoUrl.trim()} (${trimmedRef})` } +const parseWrappedNumericRef = (value: string, prefix: string, suffix: string): string | null => { + if (!value.startsWith(prefix) || !value.endsWith(suffix)) { + return null + } + const id = value.slice(prefix.length, value.length - suffix.length) + return decimalDigitsPattern.test(id) ? id : null +} + const renderWorkspaceContext = ( repoUrl: string, repoRef: string ): string => { - const issueMatch = issueRefPattern.exec(repoRef) - if (issueMatch !== null) { - const issueId = issueMatch[1] - return issueId === undefined ? renderSourceContext(repoUrl, repoRef) : renderIssueContext(repoUrl, issueId) + const issueId = parseWrappedNumericRef(repoRef, "issue-", "") + if (issueId !== null) { + return renderIssueContext(repoUrl, issueId) } - const pullMatch = githubPullRefPattern.exec(repoRef) - if (pullMatch !== null) { - const pullRequestId = pullMatch[1] - return pullRequestId === undefined - ? renderSourceContext(repoUrl, repoRef) - : renderPullRequestContext(repoUrl, pullRequestId) + const pullRequestId = parseWrappedNumericRef(repoRef, "refs/pull/", "/head") + if (pullRequestId !== null) { + return renderPullRequestContext(repoUrl, pullRequestId) } - const mergeRequestMatch = gitlabMergeRequestRefPattern.exec(repoRef) - if (mergeRequestMatch !== null) { - const mergeRequestId = mergeRequestMatch[1] - return mergeRequestId === undefined - ? renderSourceContext(repoUrl, repoRef) - : renderMergeRequestContext(mergeRequestId) + const mergeRequestId = parseWrappedNumericRef(repoRef, "refs/merge-requests/", "/head") + if (mergeRequestId !== null) { + return renderMergeRequestContext(mergeRequestId) } return renderSourceContext(repoUrl, repoRef) } diff --git a/packages/terminal/tests/core/project-terminal-label.test.ts b/packages/terminal/tests/core/project-terminal-label.test.ts index 0b6da1bd..bcaf85e2 100644 --- a/packages/terminal/tests/core/project-terminal-label.test.ts +++ b/packages/terminal/tests/core/project-terminal-label.test.ts @@ -1,7 +1,59 @@ +import * as fc from "fast-check" import { describe, expect, it } from "vitest" import { projectTerminalLabel } from "../../src/core/project-terminal-label.js" +const asciiCodeToCharacter = (code: number): string => String.fromCodePoint(code) + +const alphaNumericCharacterArbitrary = fc.oneof( + fc.integer({ max: 57, min: 48 }), + fc.integer({ max: 90, min: 65 }), + fc.integer({ max: 122, min: 97 }) +).map((code) => asciiCodeToCharacter(code)) + +const pathCharacterArbitrary = fc.oneof(alphaNumericCharacterArbitrary, fc.constant("-")) + +const labelCharacterArbitrary = fc.oneof( + pathCharacterArbitrary, + fc.constant("_"), + fc.constant("."), + fc.constant("/") +) + +const gitHubPathSegmentArbitrary = fc.tuple( + alphaNumericCharacterArbitrary, + fc.array(pathCharacterArbitrary, { maxLength: 12 }) +).map(([head, tail]) => `${head}${tail.join("")}`) + +const readableLabelArbitrary = fc.array(labelCharacterArbitrary, { + maxLength: 24, + minLength: 1 +}).map((characters) => characters.join("")) + +const paddedReadableLabelArbitrary = fc.tuple( + fc.constantFrom("", " ", " "), + readableLabelArbitrary, + fc.constantFrom("", " ", " ") +).map(([left, value, right]) => `${left}${value}${right}`) + +const repositoryArbitrary = fc.record({ + owner: gitHubPathSegmentArbitrary, + repo: gitHubPathSegmentArbitrary +}) + +type GeneratedRepository = { + readonly owner: string + readonly repo: string +} + +const refIdArbitrary = fc.integer({ max: 1_000_000, min: 1 }) + +const assertRepositoryRefIdProperty = ( + assertion: (repository: GeneratedRepository, refId: number) => void +): void => { + fc.assert(fc.property(repositoryArbitrary, refIdArbitrary, assertion)) +} + describe("projectTerminalLabel", () => { it("renders GitHub issue source context and container identity", () => { expect(projectTerminalLabel({ @@ -28,4 +80,85 @@ describe("projectTerminalLabel", () => { repoUrl: "https://github.com/org/repo.git" })).toBe("org/repo | source https://github.com/org/repo.git (feature-x)") }) + + it("preserves issue markers and GitHub issue URLs for generated issue refs", () => { + assertRepositoryRefIdProperty(({ owner, repo }, issueId) => { + const label = projectTerminalLabel({ + displayName: `${owner}/${repo}`, + repoRef: `issue-${issueId}`, + repoUrl: `https://github.com/${owner}/${repo}.git` + }) + + expect(label).toBe( + `${owner}/${repo} | issue #${issueId} (https://github.com/${owner}/${repo}/issues/${issueId})` + ) + }) + }) + + it("preserves PR and MR markers for generated review refs", () => { + fc.assert( + fc.property( + repositoryArbitrary, + refIdArbitrary, + fc.constantFrom("pull", "merge-request"), + ({ owner, repo }, reviewId, refKind) => { + const repoUrl = `git@github.com:${owner}/${repo}.git` + const label = projectTerminalLabel({ + displayName: `${owner}/${repo}`, + repoRef: refKind === "pull" ? `refs/pull/${reviewId}/head` : `refs/merge-requests/${reviewId}/head`, + repoUrl + }) + + expect(label).toBe( + refKind === "pull" + ? `${owner}/${repo} | PR #${reviewId} (https://github.com/${owner}/${repo}/pull/${reviewId})` + : `${owner}/${repo} | MR #${reviewId}` + ) + } + ) + ) + }) + + it("uses repoUrl as the base label when displayName is blank", () => { + fc.assert( + fc.property(repositoryArbitrary, fc.constantFrom("", " ", " "), ({ owner, repo }, displayName) => { + const repoUrl = `https://github.com/${owner}/${repo}.git` + + expect(projectTerminalLabel({ + displayName, + repoRef: "main", + repoUrl + })).toBe(`${repoUrl} | source ${repoUrl}`) + }) + ) + }) + + it("normalizes empty and main refs to source context without ref suffix", () => { + fc.assert( + fc.property(repositoryArbitrary, fc.constantFrom("", " ", " ", "main"), ({ owner, repo }, repoRef) => { + const repoUrl = `https://github.com/${owner}/${repo}.git` + + expect(projectTerminalLabel({ + displayName: `${owner}/${repo}`, + repoRef, + repoUrl + })).toBe(`${owner}/${repo} | source ${repoUrl}`) + }) + ) + }) + + it("preserves non-empty container names after trimming", () => { + fc.assert( + fc.property(repositoryArbitrary, paddedReadableLabelArbitrary, ({ owner, repo }, containerName) => { + const label = projectTerminalLabel({ + containerName, + displayName: `${owner}/${repo}`, + repoRef: "feature-x", + repoUrl: `https://github.com/${owner}/${repo}.git` + }) + + expect(label.endsWith(` | container ${containerName.trim()}`)).toBe(true) + }) + ) + }) })