|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + * Licensed under the MIT License. See LICENSE in the project root for license information. |
| 4 | + *--------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +const assert = require('node:assert/strict'); |
| 7 | +const { readFileSync } = require('node:fs'); |
| 8 | +const path = require('node:path'); |
| 9 | +const test = require('node:test'); |
| 10 | +const vm = require('node:vm'); |
| 11 | +const ts = require('typescript'); |
| 12 | + |
| 13 | +const configPath = path.join(__dirname, '..', 'tsconfig.json'); |
| 14 | +const config = ts.readConfigFile(configPath, ts.sys.readFile); |
| 15 | +assert.equal(config.error, undefined); |
| 16 | +const project = ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(configPath)); |
| 17 | +assert.deepEqual(project.errors, []); |
| 18 | +const compiled = ts.transpileModule(readFileSync(path.join(__dirname, 'utils.ts'), 'utf8'), { |
| 19 | + fileName: 'utils.ts', |
| 20 | + compilerOptions: project.options, |
| 21 | + reportDiagnostics: true, |
| 22 | +}); |
| 23 | +assert.deepEqual(compiled.diagnostics, []); |
| 24 | +const committed = readFileSync(path.join(__dirname, 'utils.js'), 'utf8'); |
| 25 | + |
| 26 | +function printJavaScript(source) { |
| 27 | + const tree = ts.createSourceFile('utils.js', source, ts.ScriptTarget.ES2019, true, ts.ScriptKind.JS); |
| 28 | + assert.deepEqual(tree.parseDiagnostics, []); |
| 29 | + return ts.createPrinter({ removeComments: true }).printFile(tree); |
| 30 | +} |
| 31 | + |
| 32 | +test('committed utils.js matches the TypeScript output', () => { |
| 33 | + assert.equal(printJavaScript(committed), printJavaScript(compiled.outputText)); |
| 34 | +}); |
| 35 | + |
| 36 | +function loadUtils(source, errorLogIssueNumber = '99') { |
| 37 | + const context = { |
| 38 | + repo: { owner: 'example', repo: 'triage' }, |
| 39 | + workflow: 'Helper tests', |
| 40 | + issue: { number: 7 }, |
| 41 | + payload: { action: 'labeled' }, |
| 42 | + }; |
| 43 | + const requests = []; |
| 44 | + const comments = []; |
| 45 | + const delays = []; |
| 46 | + const logs = []; |
| 47 | + const modules = { |
| 48 | + '@actions/core': { |
| 49 | + getInput: (name) => name === 'errorLogIssueNumber' ? errorLogIssueNumber : '', |
| 50 | + }, |
| 51 | + '@actions/github': { context }, |
| 52 | + axios: {}, |
| 53 | + '../api/octokit': { |
| 54 | + OctoKitIssue: class { |
| 55 | + constructor(token, repo, issue) { |
| 56 | + requests.push(JSON.parse(JSON.stringify({ token, repo, issue }))); |
| 57 | + } |
| 58 | + async postComment(body) { |
| 59 | + comments.push(body); |
| 60 | + } |
| 61 | + }, |
| 62 | + }, |
| 63 | + }; |
| 64 | + const exports = {}; |
| 65 | + new vm.Script(source, { filename: 'utils.js' }).runInNewContext({ |
| 66 | + exports, |
| 67 | + require: (name) => { |
| 68 | + assert.ok(Object.hasOwn(modules, name), `Unexpected dependency: ${name}`); |
| 69 | + return modules[name]; |
| 70 | + }, |
| 71 | + console: { |
| 72 | + log: (...args) => logs.push(args), |
| 73 | + error: (error) => assert.fail(`Unexpected error: ${error}`), |
| 74 | + }, |
| 75 | + setTimeout: (callback, delay) => { |
| 76 | + delays.push(delay); |
| 77 | + callback(); |
| 78 | + }, |
| 79 | + }, { timeout: 1000 }); |
| 80 | + return { utils: exports, context, requests, comments, delays, logs }; |
| 81 | +} |
| 82 | + |
| 83 | +for (const [name, source] of [ |
| 84 | + ['committed JavaScript', committed], |
| 85 | + ['TypeScript output', compiled.outputText], |
| 86 | +]) { |
| 87 | + test(`${name}: normalizes ordinary issue text`, () => { |
| 88 | + const { utils } = loadUtils(source); |
| 89 | + const result = utils.normalizeIssue({ body: 'Ordinary body', title: 'Useful title' }); |
| 90 | + assert.equal(result.body, 'ordinary body'); |
| 91 | + assert.equal(result.title, 'useful title'); |
| 92 | + assert.equal(result.issueType, 'unknown'); |
| 93 | + }); |
| 94 | + |
| 95 | + test(`${name}: preserves text between separate comments`, () => { |
| 96 | + const { utils } = loadUtils(source); |
| 97 | + const result = utils.normalizeIssue({ |
| 98 | + body: 'Start <!-- first note --> keep <!-- second note --> end', |
| 99 | + title: 'A <!-- note --> title', |
| 100 | + }); |
| 101 | + assert.equal(result.body, 'start keep end'); |
| 102 | + assert.equal(result.title, 'a title'); |
| 103 | + }); |
| 104 | + |
| 105 | + test(`${name}: removes ordinary multiline comments`, () => { |
| 106 | + const { utils } = loadUtils(source); |
| 107 | + const result = utils.normalizeIssue({ |
| 108 | + body: 'Start <!-- first line\nsecond line --> end', |
| 109 | + title: 'Title', |
| 110 | + }); |
| 111 | + assert.equal(result.body, 'start end'); |
| 112 | + assert.equal(result.title, 'title'); |
| 113 | + }); |
| 114 | + |
| 115 | + test(`${name}: removes comments after unrelated closing markers`, () => { |
| 116 | + const { utils } = loadUtils(source); |
| 117 | + const result = utils.normalizeIssue({ |
| 118 | + body: 'Start --> keep <!-- first note --> middle --> text <!-- second note --> end', |
| 119 | + title: 'Title --> <!-- note --> text', |
| 120 | + }); |
| 121 | + assert.equal(result.body, 'start --> keep middle --> text end'); |
| 122 | + assert.equal(result.title, 'title --> text'); |
| 123 | + }); |
| 124 | + |
| 125 | + test(`${name}: preserves incomplete comment text`, () => { |
| 126 | + const { utils } = loadUtils(source); |
| 127 | + for (const [text, expected] of [ |
| 128 | + ['Start --> end', 'start --> end'], |
| 129 | + ['Start <!-- note', 'start <!-- note'], |
| 130 | + ['Start --> text <!-- note', 'start --> text <!-- note'], |
| 131 | + ['Start <!-- complete --> tail <!-- note', 'start tail <!-- note'], |
| 132 | + ['Start <!----><!-- note --> end', 'start end'], |
| 133 | + ]) { |
| 134 | + const result = utils.normalizeIssue({ body: text, title: text }); |
| 135 | + assert.equal(result.body, expected); |
| 136 | + assert.equal(result.title, expected); |
| 137 | + } |
| 138 | + }); |
| 139 | + |
| 140 | + test(`${name}: preserves issue classification`, () => { |
| 141 | + const { utils } = loadUtils(source); |
| 142 | + for (const [body, issueType] of [ |
| 143 | + ['Issue Type: Bug', 'bug'], |
| 144 | + ['Issue Type: Feature Request', 'feature_request'], |
| 145 | + ['Ordinary issue', 'unknown'], |
| 146 | + ]) { |
| 147 | + assert.equal(utils.normalizeIssue({ body, title: 'Title' }).issueType, issueType); |
| 148 | + } |
| 149 | + }); |
| 150 | + |
| 151 | + test(`${name}: reports an ordinary error through the stubbed client`, async () => { |
| 152 | + for (const ping of [false, true]) { |
| 153 | + const runtime = loadUtils(source); |
| 154 | + await runtime.utils.logErrorToIssue('Routine test failure', ping, ''); |
| 155 | + assert.deepEqual(runtime.requests, [{ |
| 156 | + token: '', |
| 157 | + repo: { owner: 'example', repo: 'triage' }, |
| 158 | + issue: { number: 99 }, |
| 159 | + }]); |
| 160 | + assert.deepEqual(runtime.delays, [10000]); |
| 161 | + assert.equal(runtime.comments.length, 1); |
| 162 | + const comment = runtime.comments[0]; |
| 163 | + assert.ok(comment.includes('Workflow: Helper tests')); |
| 164 | + assert.ok(comment.includes('Error: Routine test failure')); |
| 165 | + assert.ok(comment.includes(`Issue: ${ping ? 'example/triage#' : ''}7`)); |
| 166 | + assert.ok(comment.includes('Repo: example/triage')); |
| 167 | + assert.ok(comment.includes(JSON.stringify(runtime.context, null, 2))); |
| 168 | + } |
| 169 | + }); |
| 170 | + |
| 171 | + test(`${name}: skips reporting when no destination is configured`, async () => { |
| 172 | + const runtime = loadUtils(source, ''); |
| 173 | + await runtime.utils.logErrorToIssue('Routine test failure', false, ''); |
| 174 | + assert.deepEqual(runtime.requests, []); |
| 175 | + assert.deepEqual(runtime.comments, []); |
| 176 | + assert.deepEqual(runtime.logs, [['no error logging repo defined. swallowing error.']]); |
| 177 | + }); |
| 178 | +} |
0 commit comments