Skip to content

Commit d02ecf8

Browse files
committed
test(core): cover terminal label invariants
1 parent e7e5e12 commit d02ecf8

2 files changed

Lines changed: 151 additions & 19 deletions

File tree

packages/terminal/src/core/project-terminal-label.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ export type ProjectTerminalLabelInput = {
55
readonly repoUrl: string
66
}
77

8-
const issueRefPattern = /^issue-(\d+)$/u
9-
const githubPullRefPattern = /^refs\/pull\/(\d+)\/head$/u
10-
const gitlabMergeRequestRefPattern = /^refs\/merge-requests\/(\d+)\/head$/u
8+
const decimalDigitsPattern = /^\d+$/u
119

1210
const stripGitSuffix = (value: string): string => value.endsWith(".git") ? value.slice(0, -4) : value
1311

@@ -67,28 +65,29 @@ const renderSourceContext = (repoUrl: string, repoRef: string): string => {
6765
: `source ${repoUrl.trim()} (${trimmedRef})`
6866
}
6967

68+
const parseWrappedNumericRef = (value: string, prefix: string, suffix: string): string | null => {
69+
if (!value.startsWith(prefix) || !value.endsWith(suffix)) {
70+
return null
71+
}
72+
const id = value.slice(prefix.length, value.length - suffix.length)
73+
return decimalDigitsPattern.test(id) ? id : null
74+
}
75+
7076
const renderWorkspaceContext = (
7177
repoUrl: string,
7278
repoRef: string
7379
): string => {
74-
const issueMatch = issueRefPattern.exec(repoRef)
75-
if (issueMatch !== null) {
76-
const issueId = issueMatch[1]
77-
return issueId === undefined ? renderSourceContext(repoUrl, repoRef) : renderIssueContext(repoUrl, issueId)
80+
const issueId = parseWrappedNumericRef(repoRef, "issue-", "")
81+
if (issueId !== null) {
82+
return renderIssueContext(repoUrl, issueId)
7883
}
79-
const pullMatch = githubPullRefPattern.exec(repoRef)
80-
if (pullMatch !== null) {
81-
const pullRequestId = pullMatch[1]
82-
return pullRequestId === undefined
83-
? renderSourceContext(repoUrl, repoRef)
84-
: renderPullRequestContext(repoUrl, pullRequestId)
84+
const pullRequestId = parseWrappedNumericRef(repoRef, "refs/pull/", "/head")
85+
if (pullRequestId !== null) {
86+
return renderPullRequestContext(repoUrl, pullRequestId)
8587
}
86-
const mergeRequestMatch = gitlabMergeRequestRefPattern.exec(repoRef)
87-
if (mergeRequestMatch !== null) {
88-
const mergeRequestId = mergeRequestMatch[1]
89-
return mergeRequestId === undefined
90-
? renderSourceContext(repoUrl, repoRef)
91-
: renderMergeRequestContext(mergeRequestId)
88+
const mergeRequestId = parseWrappedNumericRef(repoRef, "refs/merge-requests/", "/head")
89+
if (mergeRequestId !== null) {
90+
return renderMergeRequestContext(mergeRequestId)
9291
}
9392
return renderSourceContext(repoUrl, repoRef)
9493
}

packages/terminal/tests/core/project-terminal-label.test.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,59 @@
1+
import * as fc from "fast-check"
12
import { describe, expect, it } from "vitest"
23

34
import { projectTerminalLabel } from "../../src/core/project-terminal-label.js"
45

6+
const asciiCodeToCharacter = (code: number): string => String.fromCodePoint(code)
7+
8+
const alphaNumericCharacterArbitrary = fc.oneof(
9+
fc.integer({ max: 57, min: 48 }),
10+
fc.integer({ max: 90, min: 65 }),
11+
fc.integer({ max: 122, min: 97 })
12+
).map((code) => asciiCodeToCharacter(code))
13+
14+
const pathCharacterArbitrary = fc.oneof(alphaNumericCharacterArbitrary, fc.constant("-"))
15+
16+
const labelCharacterArbitrary = fc.oneof(
17+
pathCharacterArbitrary,
18+
fc.constant("_"),
19+
fc.constant("."),
20+
fc.constant("/")
21+
)
22+
23+
const gitHubPathSegmentArbitrary = fc.tuple(
24+
alphaNumericCharacterArbitrary,
25+
fc.array(pathCharacterArbitrary, { maxLength: 12 })
26+
).map(([head, tail]) => `${head}${tail.join("")}`)
27+
28+
const readableLabelArbitrary = fc.array(labelCharacterArbitrary, {
29+
maxLength: 24,
30+
minLength: 1
31+
}).map((characters) => characters.join(""))
32+
33+
const paddedReadableLabelArbitrary = fc.tuple(
34+
fc.constantFrom("", " ", " "),
35+
readableLabelArbitrary,
36+
fc.constantFrom("", " ", " ")
37+
).map(([left, value, right]) => `${left}${value}${right}`)
38+
39+
const repositoryArbitrary = fc.record({
40+
owner: gitHubPathSegmentArbitrary,
41+
repo: gitHubPathSegmentArbitrary
42+
})
43+
44+
type GeneratedRepository = {
45+
readonly owner: string
46+
readonly repo: string
47+
}
48+
49+
const refIdArbitrary = fc.integer({ max: 1_000_000, min: 1 })
50+
51+
const assertRepositoryRefIdProperty = (
52+
assertion: (repository: GeneratedRepository, refId: number) => void
53+
): void => {
54+
fc.assert(fc.property(repositoryArbitrary, refIdArbitrary, assertion))
55+
}
56+
557
describe("projectTerminalLabel", () => {
658
it("renders GitHub issue source context and container identity", () => {
759
expect(projectTerminalLabel({
@@ -28,4 +80,85 @@ describe("projectTerminalLabel", () => {
2880
repoUrl: "https://github.com/org/repo.git"
2981
})).toBe("org/repo | source https://github.com/org/repo.git (feature-x)")
3082
})
83+
84+
it("preserves issue markers and GitHub issue URLs for generated issue refs", () => {
85+
assertRepositoryRefIdProperty(({ owner, repo }, issueId) => {
86+
const label = projectTerminalLabel({
87+
displayName: `${owner}/${repo}`,
88+
repoRef: `issue-${issueId}`,
89+
repoUrl: `https://github.com/${owner}/${repo}.git`
90+
})
91+
92+
expect(label).toBe(
93+
`${owner}/${repo} | issue #${issueId} (https://github.com/${owner}/${repo}/issues/${issueId})`
94+
)
95+
})
96+
})
97+
98+
it("preserves PR and MR markers for generated review refs", () => {
99+
fc.assert(
100+
fc.property(
101+
repositoryArbitrary,
102+
refIdArbitrary,
103+
fc.constantFrom("pull", "merge-request"),
104+
({ owner, repo }, reviewId, refKind) => {
105+
const repoUrl = `git@github.com:${owner}/${repo}.git`
106+
const label = projectTerminalLabel({
107+
displayName: `${owner}/${repo}`,
108+
repoRef: refKind === "pull" ? `refs/pull/${reviewId}/head` : `refs/merge-requests/${reviewId}/head`,
109+
repoUrl
110+
})
111+
112+
expect(label).toBe(
113+
refKind === "pull"
114+
? `${owner}/${repo} | PR #${reviewId} (https://github.com/${owner}/${repo}/pull/${reviewId})`
115+
: `${owner}/${repo} | MR #${reviewId}`
116+
)
117+
}
118+
)
119+
)
120+
})
121+
122+
it("uses repoUrl as the base label when displayName is blank", () => {
123+
fc.assert(
124+
fc.property(repositoryArbitrary, fc.constantFrom("", " ", " "), ({ owner, repo }, displayName) => {
125+
const repoUrl = `https://github.com/${owner}/${repo}.git`
126+
127+
expect(projectTerminalLabel({
128+
displayName,
129+
repoRef: "main",
130+
repoUrl
131+
})).toBe(`${repoUrl} | source ${repoUrl}`)
132+
})
133+
)
134+
})
135+
136+
it("normalizes empty and main refs to source context without ref suffix", () => {
137+
fc.assert(
138+
fc.property(repositoryArbitrary, fc.constantFrom("", " ", " ", "main"), ({ owner, repo }, repoRef) => {
139+
const repoUrl = `https://github.com/${owner}/${repo}.git`
140+
141+
expect(projectTerminalLabel({
142+
displayName: `${owner}/${repo}`,
143+
repoRef,
144+
repoUrl
145+
})).toBe(`${owner}/${repo} | source ${repoUrl}`)
146+
})
147+
)
148+
})
149+
150+
it("preserves non-empty container names after trimming", () => {
151+
fc.assert(
152+
fc.property(repositoryArbitrary, paddedReadableLabelArbitrary, ({ owner, repo }, containerName) => {
153+
const label = projectTerminalLabel({
154+
containerName,
155+
displayName: `${owner}/${repo}`,
156+
repoRef: "feature-x",
157+
repoUrl: `https://github.com/${owner}/${repo}.git`
158+
})
159+
160+
expect(label.endsWith(` | container ${containerName.trim()}`)).toBe(true)
161+
})
162+
)
163+
})
31164
})

0 commit comments

Comments
 (0)