Skip to content

Commit db6586e

Browse files
feat: add optional maximum-occurrences parameter for keyphrase validation (#25)
* Initial plan * Implement maximum-occurrences feature with comprehensive testing Co-authored-by: FidelusAleksander <63016446+FidelusAleksander@users.noreply.github.com> * Add positive and negative test cases for maximum-occurrences in CI workflow Co-authored-by: FidelusAleksander <63016446+FidelusAleksander@users.noreply.github.com> * Add positive test case for exact occurrences validation Co-authored-by: FidelusAleksander <63016446+FidelusAleksander@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: FidelusAleksander <63016446+FidelusAleksander@users.noreply.github.com>
1 parent 8a4b4ee commit db6586e

7 files changed

Lines changed: 415 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,23 @@ jobs:
6666
keyphrase: 'github'
6767
minimum-occurrences: '3'
6868

69+
- name: Test maximum-occurrences (positive case)
70+
id: test-max-occurrences-positive
71+
uses: ./
72+
with:
73+
text-file: ./__tests__/test-two-occurrences.md
74+
keyphrase: 'github'
75+
maximum-occurrences: '3'
76+
77+
- name: Test exact occurrences (positive case)
78+
id: test-exact-occurrences-positive
79+
uses: ./
80+
with:
81+
text-file: ./__tests__/test-two-occurrences.md
82+
keyphrase: 'github'
83+
minimum-occurrences: '2'
84+
maximum-occurrences: '2'
85+
6986
negative-tests:
7087
name: Negative Action Tests
7188
runs-on: ubuntu-latest
@@ -111,3 +128,23 @@ jobs:
111128
echo "Expected 1 occurrences but found ${{ steps.test-text-file.outputs.occurrences }}"
112129
exit 1
113130
fi
131+
132+
- name: Test maximum-occurrences (negative case)
133+
id: test-max-occurrences-negative
134+
continue-on-error: true
135+
uses: ./
136+
with:
137+
text-file: ./__tests__/test-mixed-case.md
138+
keyphrase: 'github'
139+
maximum-occurrences: '2'
140+
141+
- name: Check output for maximum-occurrences test
142+
run: |
143+
if [[ "${{ steps.test-max-occurrences-negative.outcome }}" != "failure" ]]; then
144+
echo "Expected the test to fail but it passed"
145+
exit 1
146+
fi
147+
if [[ "${{ steps.test-max-occurrences-negative.outputs.occurrences }}" != "3" ]]; then
148+
echo "Expected 3 occurrences but found ${{ steps.test-max-occurrences-negative.outputs.occurrences }}"
149+
exit 1
150+
fi

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,47 @@ steps:
3838
minimum-occurrences: 2
3939
```
4040
41+
### Check Maximum Occurrences
42+
43+
```yaml
44+
steps:
45+
- name: Check keyphrase doesn't appear too often
46+
id: keyphrase-check
47+
uses: skills/action-keyphrase-checker@v1
48+
with:
49+
text-file: 'docs/content.md'
50+
keyphrase: 'amazing'
51+
maximum-occurrences: 3
52+
```
53+
54+
### Range Validation (Both Min and Max)
55+
56+
```yaml
57+
steps:
58+
- name: Check keyphrase appears in acceptable range
59+
id: keyphrase-check
60+
uses: skills/action-keyphrase-checker@v1
61+
with:
62+
text-file: 'docs/content.md'
63+
keyphrase: 'GitHub'
64+
minimum-occurrences: 2
65+
maximum-occurrences: 5
66+
```
67+
68+
### Exact Occurrences Check
69+
70+
```yaml
71+
steps:
72+
- name: Check for exact number of occurrences
73+
id: keyphrase-check
74+
uses: skills/action-keyphrase-checker@v1
75+
with:
76+
text-file: 'docs/content.md'
77+
keyphrase: 'TODO'
78+
minimum-occurrences: 0
79+
maximum-occurrences: 0
80+
```
81+
4182
## Inputs ⚙️
4283
4384
| Input | Description | Required | Default |
@@ -47,6 +88,7 @@ steps:
4788
| `keyphrase` | The phrase to search for in the text | Yes | - |
4889
| `case-sensitive` | Whether to perform case-sensitive matching | No | `false` |
4990
| `minimum-occurrences` | Minimum number of occurrences required for success | No | `1` |
91+
| `maximum-occurrences` | Maximum number of keyphrase occurrences allowed | No | - |
5092

5193
> \*Note: You must provide exactly one of `text-file` or `text`.
5294

__tests__/main.test.ts

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,269 @@ describe('Keyphrase Checker Action', () => {
212212
"Exactly one of 'text-file' or 'text' inputs must be provided"
213213
)
214214
})
215+
216+
// Tests for maximum-occurrences feature
217+
test('passes when occurrences are within maximum limit', async () => {
218+
// Get the path to the test file
219+
const testFilePath = path.join(__dirname, 'test-two-occurrences.md')
220+
221+
// Setup mocks
222+
core.getInput.mockImplementation((name) => {
223+
switch (name) {
224+
case 'text-file':
225+
return testFilePath
226+
case 'keyphrase':
227+
return 'GitHub'
228+
case 'minimum-occurrences':
229+
return '1'
230+
case 'maximum-occurrences':
231+
return '3'
232+
default:
233+
return ''
234+
}
235+
})
236+
core.getBooleanInput.mockReturnValue(true)
237+
238+
// Run the action
239+
await run()
240+
241+
// Check expectations
242+
expect(core.setOutput).toHaveBeenCalledWith('occurrences', 2)
243+
expect(core.setFailed).not.toHaveBeenCalled()
244+
})
245+
246+
test('fails when occurrences exceed maximum limit', async () => {
247+
// Get the path to the test file
248+
const testFilePath = path.join(__dirname, 'test-two-occurrences.md')
249+
250+
// Setup mocks
251+
core.getInput.mockImplementation((name) => {
252+
switch (name) {
253+
case 'text-file':
254+
return testFilePath
255+
case 'keyphrase':
256+
return 'GitHub'
257+
case 'minimum-occurrences':
258+
return '1'
259+
case 'maximum-occurrences':
260+
return '1'
261+
default:
262+
return ''
263+
}
264+
})
265+
core.getBooleanInput.mockReturnValue(true)
266+
267+
// Run the action
268+
await run()
269+
270+
// Check expectations
271+
expect(core.setOutput).toHaveBeenCalledWith('occurrences', 2)
272+
expect(core.setFailed).toHaveBeenCalledWith(
273+
'Expected at most 1 occurrences of "GitHub", but found 2'
274+
)
275+
})
276+
277+
test('passes with exact occurrences when min equals max', async () => {
278+
// Get the path to the test file
279+
const testFilePath = path.join(__dirname, 'test-two-occurrences.md')
280+
281+
// Setup mocks
282+
core.getInput.mockImplementation((name) => {
283+
switch (name) {
284+
case 'text-file':
285+
return testFilePath
286+
case 'keyphrase':
287+
return 'GitHub'
288+
case 'minimum-occurrences':
289+
return '2'
290+
case 'maximum-occurrences':
291+
return '2'
292+
default:
293+
return ''
294+
}
295+
})
296+
core.getBooleanInput.mockReturnValue(true)
297+
298+
// Run the action
299+
await run()
300+
301+
// Check expectations
302+
expect(core.setOutput).toHaveBeenCalledWith('occurrences', 2)
303+
expect(core.setFailed).not.toHaveBeenCalled()
304+
})
305+
306+
test('fails when maximum is less than minimum', async () => {
307+
// Get the path to the test file
308+
const testFilePath = path.join(__dirname, 'test-two-occurrences.md')
309+
310+
// Setup mocks
311+
core.getInput.mockImplementation((name) => {
312+
switch (name) {
313+
case 'text-file':
314+
return testFilePath
315+
case 'keyphrase':
316+
return 'GitHub'
317+
case 'minimum-occurrences':
318+
return '3'
319+
case 'maximum-occurrences':
320+
return '2'
321+
default:
322+
return ''
323+
}
324+
})
325+
core.getBooleanInput.mockReturnValue(true)
326+
327+
// Run the action
328+
await run()
329+
330+
// Check expectations
331+
expect(core.setFailed).toHaveBeenCalledWith(
332+
'Invalid configuration: maximum-occurrences (2) must be greater than or equal to minimum-occurrences (3)'
333+
)
334+
})
335+
336+
test('passes with range validation - occurrences within range', async () => {
337+
// Get the path to the test file with mixed case occurrences (3 total)
338+
const testFilePath = path.join(__dirname, 'test-mixed-case.md')
339+
340+
// Setup mocks
341+
core.getInput.mockImplementation((name) => {
342+
switch (name) {
343+
case 'text-file':
344+
return testFilePath
345+
case 'keyphrase':
346+
return 'GitHub'
347+
case 'minimum-occurrences':
348+
return '2'
349+
case 'maximum-occurrences':
350+
return '5'
351+
default:
352+
return ''
353+
}
354+
})
355+
core.getBooleanInput.mockReturnValue(false)
356+
357+
// Run the action
358+
await run()
359+
360+
// Check expectations
361+
expect(core.setOutput).toHaveBeenCalledWith('occurrences', 3)
362+
expect(core.setFailed).not.toHaveBeenCalled()
363+
})
364+
365+
test('fails when occurrences are below minimum in range validation', async () => {
366+
// Get the path to the test file
367+
const testFilePath = path.join(__dirname, 'test-two-occurrences.md')
368+
369+
// Setup mocks
370+
core.getInput.mockImplementation((name) => {
371+
switch (name) {
372+
case 'text-file':
373+
return testFilePath
374+
case 'keyphrase':
375+
return 'GitHub'
376+
case 'minimum-occurrences':
377+
return '3'
378+
case 'maximum-occurrences':
379+
return '5'
380+
default:
381+
return ''
382+
}
383+
})
384+
core.getBooleanInput.mockReturnValue(true)
385+
386+
// Run the action
387+
await run()
388+
389+
// Check expectations
390+
expect(core.setOutput).toHaveBeenCalledWith('occurrences', 2)
391+
expect(core.setFailed).toHaveBeenCalledWith(
392+
'Expected at least 3 occurrences of "GitHub", but found only 2'
393+
)
394+
})
395+
396+
test('fails when occurrences are above maximum in range validation', async () => {
397+
// Get the path to the test file with mixed case occurrences (3 total)
398+
const testFilePath = path.join(__dirname, 'test-mixed-case.md')
399+
400+
// Setup mocks
401+
core.getInput.mockImplementation((name) => {
402+
switch (name) {
403+
case 'text-file':
404+
return testFilePath
405+
case 'keyphrase':
406+
return 'GitHub'
407+
case 'minimum-occurrences':
408+
return '1'
409+
case 'maximum-occurrences':
410+
return '2'
411+
default:
412+
return ''
413+
}
414+
})
415+
core.getBooleanInput.mockReturnValue(false)
416+
417+
// Run the action
418+
await run()
419+
420+
// Check expectations
421+
expect(core.setOutput).toHaveBeenCalledWith('occurrences', 3)
422+
expect(core.setFailed).toHaveBeenCalledWith(
423+
'Expected at most 2 occurrences of "GitHub", but found 3'
424+
)
425+
})
426+
427+
test('works with maximum-occurrences set to 0', async () => {
428+
// Setup mocks for text without the keyphrase
429+
core.getInput.mockImplementation((name) => {
430+
switch (name) {
431+
case 'text':
432+
return 'This text does not contain the searched term'
433+
case 'keyphrase':
434+
return 'GitHub'
435+
case 'minimum-occurrences':
436+
return '0'
437+
case 'maximum-occurrences':
438+
return '0'
439+
default:
440+
return ''
441+
}
442+
})
443+
core.getBooleanInput.mockReturnValue(true)
444+
445+
// Run the action
446+
await run()
447+
448+
// Check expectations
449+
expect(core.setOutput).toHaveBeenCalledWith('occurrences', 0)
450+
expect(core.setFailed).not.toHaveBeenCalled()
451+
})
452+
453+
test('fails when maximum-occurrences is 0 but keyphrase is found', async () => {
454+
// Setup mocks
455+
core.getInput.mockImplementation((name) => {
456+
switch (name) {
457+
case 'text':
458+
return 'This text contains GitHub'
459+
case 'keyphrase':
460+
return 'GitHub'
461+
case 'minimum-occurrences':
462+
return '0'
463+
case 'maximum-occurrences':
464+
return '0'
465+
default:
466+
return ''
467+
}
468+
})
469+
core.getBooleanInput.mockReturnValue(true)
470+
471+
// Run the action
472+
await run()
473+
474+
// Check expectations
475+
expect(core.setOutput).toHaveBeenCalledWith('occurrences', 1)
476+
expect(core.setFailed).toHaveBeenCalledWith(
477+
'Expected at most 0 occurrences of "GitHub", but found 1'
478+
)
479+
})
215480
})

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ inputs:
2121
description: 'Minimum number of occurrences required for success'
2222
required: true
2323
default: '1'
24+
maximum-occurrences:
25+
description: 'Maximum number of keyphrase occurrences allowed'
26+
required: false
2427

2528
outputs:
2629
occurrences:

0 commit comments

Comments
 (0)