From 7f95f29db30d0591f2970353f6409ad7d8497af4 Mon Sep 17 00:00:00 2001 From: Noah Santschi-Cooney Date: Thu, 30 Apr 2026 16:50:10 +0100 Subject: [PATCH 1/3] feat(workspace): add Go workspace (go.work) discovery --- src/index.js | 13 +- src/workspace.js | 53 ++++++ .../tst_manifests/golang/go_workspace/go.work | 6 + .../golang/go_workspace/module-a/go.mod | 3 + .../golang/go_workspace/module-b/go.mod | 3 + .../existing/go.mod | 3 + .../go_workspace_missing_module/go.work | 6 + .../golang/go_workspace_nested/go.work | 6 + .../go_workspace_nested/libs/core/go.mod | 3 + .../go_workspace_nested/libs/util/go.mod | 3 + .../golang/go_workspace_single/go.work | 3 + .../golang/go_workspace_single/mymod/go.mod | 3 + test/providers/workspace.test.js | 158 ++++++++++++++++++ 13 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 test/providers/tst_manifests/golang/go_workspace/go.work create mode 100644 test/providers/tst_manifests/golang/go_workspace/module-a/go.mod create mode 100644 test/providers/tst_manifests/golang/go_workspace/module-b/go.mod create mode 100644 test/providers/tst_manifests/golang/go_workspace_missing_module/existing/go.mod create mode 100644 test/providers/tst_manifests/golang/go_workspace_missing_module/go.work create mode 100644 test/providers/tst_manifests/golang/go_workspace_nested/go.work create mode 100644 test/providers/tst_manifests/golang/go_workspace_nested/libs/core/go.mod create mode 100644 test/providers/tst_manifests/golang/go_workspace_nested/libs/util/go.mod create mode 100644 test/providers/tst_manifests/golang/go_workspace_single/go.work create mode 100644 test/providers/tst_manifests/golang/go_workspace_single/mymod/go.mod diff --git a/src/index.js b/src/index.js index 24d2ed26..10be8dfa 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ import { resolveBatchMetadata, resolveContinueOnError } from './batch_opts.js' import { discoverMavenModules } from './providers/java_maven.js' import { discoverGradleSubprojects } from './providers/java_gradle.js' import { + discoverGoWorkspaceModules, discoverWorkspaceCrates, discoverWorkspacePackages, filterManifestPathsByDiscoveryIgnore, @@ -27,6 +28,7 @@ export default { componentAnalysis, stackAnalysis, stackAnalysisBatch, imageAnal export { discoverMavenModules, discoverGradleSubprojects, + discoverGoWorkspaceModules, discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson, @@ -323,7 +325,7 @@ async function generateOneSbom(manifestPath, workspaceOpts) { * * @param {string} root - Resolved workspace root * @param {Options} opts - * @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'maven' | 'gradle' | 'unknown', manifestPaths: string[] }>} + * @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'maven' | 'gradle' | 'gomodules' | 'unknown', manifestPaths: string[] }>} * @private */ async function detectWorkspaceManifests(root, opts) { @@ -352,6 +354,13 @@ async function detectWorkspaceManifests(root, opts) { } } + if (fs.existsSync(path.join(root, 'go.work'))) { + const manifestPaths = await discoverGoWorkspaceModules(root, opts) + if (manifestPaths.length > 0) { + return { ecosystem: 'gomodules', manifestPaths } + } + } + const hasJsLock = fs.existsSync(path.join(root, 'pnpm-lock.yaml')) || fs.existsSync(path.join(root, 'yarn.lock')) || fs.existsSync(path.join(root, 'package-lock.json')) @@ -547,7 +556,7 @@ async function stackAnalysisBatch(workspaceRoot, html = false, opts = {}) { } if (manifestPaths.length === 0) { - throw new Error(`No workspace manifests found at ${root}. Ensure Cargo.toml+Cargo.lock or package.json+lock file exist.`) + throw new Error(`No workspace manifests found at ${root}. Ensure a supported workspace root exists (Cargo.toml+Cargo.lock, go.work, or package.json+lock file).`) } const workspaceOpts = { ...opts, TRUSTIFY_DA_WORKSPACE_DIR: root } diff --git a/src/workspace.js b/src/workspace.js index f71d5e18..c0217900 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -269,3 +269,56 @@ export async function discoverWorkspaceCrates(workspaceRoot, opts = {}) { const ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts) return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns) } + +/** + * Discover all go.mod manifest paths in a Go workspace. + * Uses `go work edit -json` to get workspace members. + * + * @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain go.work) + * @param {import('./index.js').Options} [opts={}] + * @returns {Promise} Paths to go.mod files (absolute) + */ +export async function discoverGoWorkspaceModules(workspaceRoot, opts = {}) { + const root = path.resolve(workspaceRoot) + const goWork = path.join(root, 'go.work') + + if (!fs.existsSync(goWork)) { + return [] + } + + const goBin = getCustomPath('go', opts) + let output + try { + output = invokeCommand(goBin, ['work', 'edit', '-json', goWork], { cwd: root }) + } catch { + return [] + } + + let workspace + try { + workspace = JSON.parse(output.toString().trim()) + } catch { + return [] + } + + const useEntries = workspace.Use || [] + if (useEntries.length === 0) { + return [] + } + + const manifestPaths = [] + for (const entry of useEntries) { + const diskPath = entry.DiskPath + if (!diskPath) { + continue + } + const moduleDir = path.resolve(root, diskPath) + const goMod = path.join(moduleDir, 'go.mod') + if (fs.existsSync(goMod)) { + manifestPaths.push(goMod) + } + } + + const ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts) + return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns) +} diff --git a/test/providers/tst_manifests/golang/go_workspace/go.work b/test/providers/tst_manifests/golang/go_workspace/go.work new file mode 100644 index 00000000..fd2b5974 --- /dev/null +++ b/test/providers/tst_manifests/golang/go_workspace/go.work @@ -0,0 +1,6 @@ +go 1.22 + +use ( + ./module-a + ./module-b +) diff --git a/test/providers/tst_manifests/golang/go_workspace/module-a/go.mod b/test/providers/tst_manifests/golang/go_workspace/module-a/go.mod new file mode 100644 index 00000000..9d0ee8b3 --- /dev/null +++ b/test/providers/tst_manifests/golang/go_workspace/module-a/go.mod @@ -0,0 +1,3 @@ +module example.com/module-a + +go 1.22 diff --git a/test/providers/tst_manifests/golang/go_workspace/module-b/go.mod b/test/providers/tst_manifests/golang/go_workspace/module-b/go.mod new file mode 100644 index 00000000..670b00fd --- /dev/null +++ b/test/providers/tst_manifests/golang/go_workspace/module-b/go.mod @@ -0,0 +1,3 @@ +module example.com/module-b + +go 1.22 diff --git a/test/providers/tst_manifests/golang/go_workspace_missing_module/existing/go.mod b/test/providers/tst_manifests/golang/go_workspace_missing_module/existing/go.mod new file mode 100644 index 00000000..7fe9faa2 --- /dev/null +++ b/test/providers/tst_manifests/golang/go_workspace_missing_module/existing/go.mod @@ -0,0 +1,3 @@ +module example.com/existing + +go 1.22 diff --git a/test/providers/tst_manifests/golang/go_workspace_missing_module/go.work b/test/providers/tst_manifests/golang/go_workspace_missing_module/go.work new file mode 100644 index 00000000..5eaa2341 --- /dev/null +++ b/test/providers/tst_manifests/golang/go_workspace_missing_module/go.work @@ -0,0 +1,6 @@ +go 1.22 + +use ( + ./existing + ./nonexistent +) diff --git a/test/providers/tst_manifests/golang/go_workspace_nested/go.work b/test/providers/tst_manifests/golang/go_workspace_nested/go.work new file mode 100644 index 00000000..50fc223c --- /dev/null +++ b/test/providers/tst_manifests/golang/go_workspace_nested/go.work @@ -0,0 +1,6 @@ +go 1.22 + +use ( + ./libs/core + ./libs/util +) diff --git a/test/providers/tst_manifests/golang/go_workspace_nested/libs/core/go.mod b/test/providers/tst_manifests/golang/go_workspace_nested/libs/core/go.mod new file mode 100644 index 00000000..2bf082f2 --- /dev/null +++ b/test/providers/tst_manifests/golang/go_workspace_nested/libs/core/go.mod @@ -0,0 +1,3 @@ +module example.com/libs/core + +go 1.22 diff --git a/test/providers/tst_manifests/golang/go_workspace_nested/libs/util/go.mod b/test/providers/tst_manifests/golang/go_workspace_nested/libs/util/go.mod new file mode 100644 index 00000000..3954c84d --- /dev/null +++ b/test/providers/tst_manifests/golang/go_workspace_nested/libs/util/go.mod @@ -0,0 +1,3 @@ +module example.com/libs/util + +go 1.22 diff --git a/test/providers/tst_manifests/golang/go_workspace_single/go.work b/test/providers/tst_manifests/golang/go_workspace_single/go.work new file mode 100644 index 00000000..241141be --- /dev/null +++ b/test/providers/tst_manifests/golang/go_workspace_single/go.work @@ -0,0 +1,3 @@ +go 1.22 + +use ./mymod diff --git a/test/providers/tst_manifests/golang/go_workspace_single/mymod/go.mod b/test/providers/tst_manifests/golang/go_workspace_single/mymod/go.mod new file mode 100644 index 00000000..f8019326 --- /dev/null +++ b/test/providers/tst_manifests/golang/go_workspace_single/mymod/go.mod @@ -0,0 +1,3 @@ +module example.com/mymod + +go 1.22 diff --git a/test/providers/workspace.test.js b/test/providers/workspace.test.js index a75b20e5..7a059552 100644 --- a/test/providers/workspace.test.js +++ b/test/providers/workspace.test.js @@ -7,6 +7,7 @@ import esmock from 'esmock' import { discoverGradleSubprojects } from '../../src/providers/java_gradle.js' import { discoverMavenModules } from '../../src/providers/java_maven.js' import { + discoverGoWorkspaceModules, discoverWorkspaceCrates, discoverWorkspacePackages, filterManifestPathsByDiscoveryIgnore, @@ -267,6 +268,7 @@ suite('discoverMavenModules', () => { expect(result).to.be.an('array') expect(result).to.have.lengthOf(0) }) + test('returns root pom only when mvn reports no modules', async () => { const root = path.resolve('test/providers/tst_manifests/maven/maven_no_modules') const result = await discoverMavenModules(root) @@ -320,3 +322,159 @@ suite('discoverMavenModules', () => { expect(result.some(p => p.includes('module-b'))).to.be.false }).timeout(40000) }) + +suite('discoverGoWorkspaceModules', () => { + test('returns empty when no go.work at root', async () => { + const result = await discoverGoWorkspaceModules('test/providers/tst_manifests/npm') + expect(result).to.be.an('array') + expect(result).to.have.lengthOf(0) + }) + + test('discovers modules from go.work with two modules', async () => { + const root = path.resolve('test/providers/tst_manifests/golang/go_workspace') + const goWorkJson = { + Go: { Version: '1.22' }, + Use: [ + { DiskPath: './module-a' }, + { DiskPath: './module-b' }, + ], + } + const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + '../../src/tools.js': { + getCustom: () => null, + getCustomPath: () => 'go', + invokeCommand: () => Buffer.from(JSON.stringify(goWorkJson)), + }, + }) + const result = await discoverGoWorkspaceModules(root) + expect(result).to.be.an('array') + expect(result).to.have.lengthOf(2) + expect(result.every(p => p.endsWith('go.mod'))).to.be.true + expect(result.some(p => p.includes('module-a'))).to.be.true + expect(result.some(p => p.includes('module-b'))).to.be.true + }) + + test('discovers modules from nested directories', async () => { + const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_nested') + const goWorkJson = { + Go: { Version: '1.22' }, + Use: [ + { DiskPath: './libs/core' }, + { DiskPath: './libs/util' }, + ], + } + const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + '../../src/tools.js': { + getCustom: () => null, + getCustomPath: () => 'go', + invokeCommand: () => Buffer.from(JSON.stringify(goWorkJson)), + }, + }) + const result = await discoverGoWorkspaceModules(root) + expect(result).to.have.lengthOf(2) + expect(result.some(p => p.includes(path.join('libs', 'core', 'go.mod')))).to.be.true + expect(result.some(p => p.includes(path.join('libs', 'util', 'go.mod')))).to.be.true + }) + + test('discovers single module', async () => { + const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_single') + const goWorkJson = { + Go: { Version: '1.22' }, + Use: [{ DiskPath: './mymod' }], + } + const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + '../../src/tools.js': { + getCustom: () => null, + getCustomPath: () => 'go', + invokeCommand: () => Buffer.from(JSON.stringify(goWorkJson)), + }, + }) + const result = await discoverGoWorkspaceModules(root) + expect(result).to.have.lengthOf(1) + expect(result[0]).to.include(path.join('mymod', 'go.mod')) + }) + + test('skips modules whose directory does not exist', async () => { + const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_missing_module') + const goWorkJson = { + Go: { Version: '1.22' }, + Use: [ + { DiskPath: './existing' }, + { DiskPath: './nonexistent' }, + ], + } + const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + '../../src/tools.js': { + getCustom: () => null, + getCustomPath: () => 'go', + invokeCommand: () => Buffer.from(JSON.stringify(goWorkJson)), + }, + }) + const result = await discoverGoWorkspaceModules(root) + expect(result).to.have.lengthOf(1) + expect(result[0]).to.include(path.join('existing', 'go.mod')) + }) + + test('returns empty when go command fails', async () => { + const root = path.resolve('test/providers/tst_manifests/golang/go_workspace') + const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + '../../src/tools.js': { + getCustom: () => null, + getCustomPath: () => 'go', + invokeCommand: () => { throw new Error('go not found') }, + }, + }) + const result = await discoverGoWorkspaceModules(root) + expect(result).to.have.lengthOf(0) + }) + + test('returns empty when go output is invalid JSON', async () => { + const root = path.resolve('test/providers/tst_manifests/golang/go_workspace') + const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + '../../src/tools.js': { + getCustom: () => null, + getCustomPath: () => 'go', + invokeCommand: () => Buffer.from('not json'), + }, + }) + const result = await discoverGoWorkspaceModules(root) + expect(result).to.have.lengthOf(0) + }) + + test('returns empty when Use array is empty', async () => { + const root = path.resolve('test/providers/tst_manifests/golang/go_workspace') + const goWorkJson = { Go: { Version: '1.22' }, Use: [] } + const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + '../../src/tools.js': { + getCustom: () => null, + getCustomPath: () => 'go', + invokeCommand: () => Buffer.from(JSON.stringify(goWorkJson)), + }, + }) + const result = await discoverGoWorkspaceModules(root) + expect(result).to.have.lengthOf(0) + }) + + test('applies ignore patterns to discovered modules', async () => { + const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_nested') + const goWorkJson = { + Go: { Version: '1.22' }, + Use: [ + { DiskPath: './libs/core' }, + { DiskPath: './libs/util' }, + ], + } + const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + '../../src/tools.js': { + getCustom: () => null, + getCustomPath: () => 'go', + invokeCommand: () => Buffer.from(JSON.stringify(goWorkJson)), + }, + }) + const result = await discoverGoWorkspaceModules(root, { + workspaceDiscoveryIgnore: ['**/util/**'], + }) + expect(result.some(p => p.includes(path.join('libs', 'core', 'go.mod')))).to.be.true + expect(result.some(p => p.includes(path.join('libs', 'util', 'go.mod')))).to.be.false + }) +}) From 13c5a8e4cc179ce38b898ef7c83ec61630c1d6da Mon Sep 17 00:00:00 2001 From: Noah Santschi-Cooney Date: Fri, 1 May 2026 13:04:25 +0100 Subject: [PATCH 2/3] refactor: move Go discovery logic from workspace.js to golang_gomodules.js Move discoverGoWorkspaceModules to its provider file so workspace.js only retains generic scaffolding. Co-Authored-By: Claude Opus 4.6 --- src/index.js | 2 +- src/providers/golang_gomodules.js | 54 +++++++++++++++++++++++++++++++ src/workspace.js | 53 ------------------------------ test/providers/workspace.test.js | 18 +++++------ 4 files changed, 64 insertions(+), 63 deletions(-) diff --git a/src/index.js b/src/index.js index 10be8dfa..138157ef 100644 --- a/src/index.js +++ b/src/index.js @@ -9,8 +9,8 @@ import { getCustom } from "./tools.js"; import { resolveBatchMetadata, resolveContinueOnError } from './batch_opts.js' import { discoverMavenModules } from './providers/java_maven.js' import { discoverGradleSubprojects } from './providers/java_gradle.js' +import { discoverGoWorkspaceModules } from './providers/golang_gomodules.js' import { - discoverGoWorkspaceModules, discoverWorkspaceCrates, discoverWorkspacePackages, filterManifestPathsByDiscoveryIgnore, diff --git a/src/providers/golang_gomodules.js b/src/providers/golang_gomodules.js index 850f1e2f..0ac3be3c 100644 --- a/src/providers/golang_gomodules.js +++ b/src/providers/golang_gomodules.js @@ -6,6 +6,7 @@ import { PackageURL } from 'packageurl-js' import { readLicenseFile } from '../license/license_utils.js' import Sbom from '../sbom.js' import { getCustom, getCustomPath, invokeCommand } from "../tools.js"; +import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore } from '../workspace.js' import { getParser, getRequireQuery } from './gomod_parser.js' @@ -427,3 +428,56 @@ function getLineSeparatorGolang() { let reg = /\n|\r\n/ return reg } + +/** + * Discover all go.mod manifest paths in a Go workspace. + * Uses `go work edit -json` to get workspace members. + * + * @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain go.work) + * @param {import('../index.js').Options} [opts={}] + * @returns {Promise} Paths to go.mod files (absolute) + */ +export async function discoverGoWorkspaceModules(workspaceRoot, opts = {}) { + const root = path.resolve(workspaceRoot) + const goWork = path.join(root, 'go.work') + + if (!fs.existsSync(goWork)) { + return [] + } + + const goBin = getCustomPath('go', opts) + let output + try { + output = invokeCommand(goBin, ['work', 'edit', '-json', goWork], { cwd: root }) + } catch { + return [] + } + + let workspace + try { + workspace = JSON.parse(output.toString().trim()) + } catch { + return [] + } + + const useEntries = workspace.Use || [] + if (useEntries.length === 0) { + return [] + } + + const manifestPaths = [] + for (const entry of useEntries) { + const diskPath = entry.DiskPath + if (!diskPath) { + continue + } + const moduleDir = path.resolve(root, diskPath) + const goMod = path.join(moduleDir, 'go.mod') + if (fs.existsSync(goMod)) { + manifestPaths.push(goMod) + } + } + + const ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts) + return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns) +} diff --git a/src/workspace.js b/src/workspace.js index c0217900..f71d5e18 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -269,56 +269,3 @@ export async function discoverWorkspaceCrates(workspaceRoot, opts = {}) { const ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts) return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns) } - -/** - * Discover all go.mod manifest paths in a Go workspace. - * Uses `go work edit -json` to get workspace members. - * - * @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain go.work) - * @param {import('./index.js').Options} [opts={}] - * @returns {Promise} Paths to go.mod files (absolute) - */ -export async function discoverGoWorkspaceModules(workspaceRoot, opts = {}) { - const root = path.resolve(workspaceRoot) - const goWork = path.join(root, 'go.work') - - if (!fs.existsSync(goWork)) { - return [] - } - - const goBin = getCustomPath('go', opts) - let output - try { - output = invokeCommand(goBin, ['work', 'edit', '-json', goWork], { cwd: root }) - } catch { - return [] - } - - let workspace - try { - workspace = JSON.parse(output.toString().trim()) - } catch { - return [] - } - - const useEntries = workspace.Use || [] - if (useEntries.length === 0) { - return [] - } - - const manifestPaths = [] - for (const entry of useEntries) { - const diskPath = entry.DiskPath - if (!diskPath) { - continue - } - const moduleDir = path.resolve(root, diskPath) - const goMod = path.join(moduleDir, 'go.mod') - if (fs.existsSync(goMod)) { - manifestPaths.push(goMod) - } - } - - const ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts) - return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns) -} diff --git a/test/providers/workspace.test.js b/test/providers/workspace.test.js index 7a059552..7aa45393 100644 --- a/test/providers/workspace.test.js +++ b/test/providers/workspace.test.js @@ -4,10 +4,10 @@ import path from 'node:path' import { expect } from 'chai' import esmock from 'esmock' +import { discoverGoWorkspaceModules } from '../../src/providers/golang_gomodules.js' import { discoverGradleSubprojects } from '../../src/providers/java_gradle.js' import { discoverMavenModules } from '../../src/providers/java_maven.js' import { - discoverGoWorkspaceModules, discoverWorkspaceCrates, discoverWorkspacePackages, filterManifestPathsByDiscoveryIgnore, @@ -339,7 +339,7 @@ suite('discoverGoWorkspaceModules', () => { { DiskPath: './module-b' }, ], } - const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { '../../src/tools.js': { getCustom: () => null, getCustomPath: () => 'go', @@ -363,7 +363,7 @@ suite('discoverGoWorkspaceModules', () => { { DiskPath: './libs/util' }, ], } - const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { '../../src/tools.js': { getCustom: () => null, getCustomPath: () => 'go', @@ -382,7 +382,7 @@ suite('discoverGoWorkspaceModules', () => { Go: { Version: '1.22' }, Use: [{ DiskPath: './mymod' }], } - const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { '../../src/tools.js': { getCustom: () => null, getCustomPath: () => 'go', @@ -403,7 +403,7 @@ suite('discoverGoWorkspaceModules', () => { { DiskPath: './nonexistent' }, ], } - const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { '../../src/tools.js': { getCustom: () => null, getCustomPath: () => 'go', @@ -417,7 +417,7 @@ suite('discoverGoWorkspaceModules', () => { test('returns empty when go command fails', async () => { const root = path.resolve('test/providers/tst_manifests/golang/go_workspace') - const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { '../../src/tools.js': { getCustom: () => null, getCustomPath: () => 'go', @@ -430,7 +430,7 @@ suite('discoverGoWorkspaceModules', () => { test('returns empty when go output is invalid JSON', async () => { const root = path.resolve('test/providers/tst_manifests/golang/go_workspace') - const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { '../../src/tools.js': { getCustom: () => null, getCustomPath: () => 'go', @@ -444,7 +444,7 @@ suite('discoverGoWorkspaceModules', () => { test('returns empty when Use array is empty', async () => { const root = path.resolve('test/providers/tst_manifests/golang/go_workspace') const goWorkJson = { Go: { Version: '1.22' }, Use: [] } - const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { '../../src/tools.js': { getCustom: () => null, getCustomPath: () => 'go', @@ -464,7 +464,7 @@ suite('discoverGoWorkspaceModules', () => { { DiskPath: './libs/util' }, ], } - const { discoverGoWorkspaceModules } = await esmock('../../src/workspace.js', { + const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { '../../src/tools.js': { getCustom: () => null, getCustomPath: () => 'go', From 04864de61ba1e4aaee99e3cd50bd31246a50c14a Mon Sep 17 00:00:00 2001 From: Noah Santschi-Cooney Date: Wed, 6 May 2026 15:00:36 +0100 Subject: [PATCH 3/3] test: use real go binary in Go workspace discovery tests Replace esmock invokeCommand mocks with real `go work edit -json` invocations for happy-path tests. Keep esmock only for error paths that cannot be reproduced with a real binary (command failure, invalid JSON output). Add go_workspace_empty fixture for the null-Use case. Co-Authored-By: Claude Opus 4.6 --- .../golang/go_workspace_empty/go.work | 1 + test/providers/workspace.test.js | 87 ++----------------- 2 files changed, 7 insertions(+), 81 deletions(-) create mode 100644 test/providers/tst_manifests/golang/go_workspace_empty/go.work diff --git a/test/providers/tst_manifests/golang/go_workspace_empty/go.work b/test/providers/tst_manifests/golang/go_workspace_empty/go.work new file mode 100644 index 00000000..233b1008 --- /dev/null +++ b/test/providers/tst_manifests/golang/go_workspace_empty/go.work @@ -0,0 +1 @@ +go 1.22 diff --git a/test/providers/workspace.test.js b/test/providers/workspace.test.js index 7aa45393..673111a6 100644 --- a/test/providers/workspace.test.js +++ b/test/providers/workspace.test.js @@ -332,20 +332,6 @@ suite('discoverGoWorkspaceModules', () => { test('discovers modules from go.work with two modules', async () => { const root = path.resolve('test/providers/tst_manifests/golang/go_workspace') - const goWorkJson = { - Go: { Version: '1.22' }, - Use: [ - { DiskPath: './module-a' }, - { DiskPath: './module-b' }, - ], - } - const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { - '../../src/tools.js': { - getCustom: () => null, - getCustomPath: () => 'go', - invokeCommand: () => Buffer.from(JSON.stringify(goWorkJson)), - }, - }) const result = await discoverGoWorkspaceModules(root) expect(result).to.be.an('array') expect(result).to.have.lengthOf(2) @@ -356,20 +342,6 @@ suite('discoverGoWorkspaceModules', () => { test('discovers modules from nested directories', async () => { const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_nested') - const goWorkJson = { - Go: { Version: '1.22' }, - Use: [ - { DiskPath: './libs/core' }, - { DiskPath: './libs/util' }, - ], - } - const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { - '../../src/tools.js': { - getCustom: () => null, - getCustomPath: () => 'go', - invokeCommand: () => Buffer.from(JSON.stringify(goWorkJson)), - }, - }) const result = await discoverGoWorkspaceModules(root) expect(result).to.have.lengthOf(2) expect(result.some(p => p.includes(path.join('libs', 'core', 'go.mod')))).to.be.true @@ -378,17 +350,6 @@ suite('discoverGoWorkspaceModules', () => { test('discovers single module', async () => { const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_single') - const goWorkJson = { - Go: { Version: '1.22' }, - Use: [{ DiskPath: './mymod' }], - } - const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { - '../../src/tools.js': { - getCustom: () => null, - getCustomPath: () => 'go', - invokeCommand: () => Buffer.from(JSON.stringify(goWorkJson)), - }, - }) const result = await discoverGoWorkspaceModules(root) expect(result).to.have.lengthOf(1) expect(result[0]).to.include(path.join('mymod', 'go.mod')) @@ -396,20 +357,6 @@ suite('discoverGoWorkspaceModules', () => { test('skips modules whose directory does not exist', async () => { const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_missing_module') - const goWorkJson = { - Go: { Version: '1.22' }, - Use: [ - { DiskPath: './existing' }, - { DiskPath: './nonexistent' }, - ], - } - const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { - '../../src/tools.js': { - getCustom: () => null, - getCustomPath: () => 'go', - invokeCommand: () => Buffer.from(JSON.stringify(goWorkJson)), - }, - }) const result = await discoverGoWorkspaceModules(root) expect(result).to.have.lengthOf(1) expect(result[0]).to.include(path.join('existing', 'go.mod')) @@ -417,60 +364,38 @@ suite('discoverGoWorkspaceModules', () => { test('returns empty when go command fails', async () => { const root = path.resolve('test/providers/tst_manifests/golang/go_workspace') - const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { + const { discoverGoWorkspaceModules: discoverMocked } = await esmock('../../src/providers/golang_gomodules.js', { '../../src/tools.js': { getCustom: () => null, getCustomPath: () => 'go', invokeCommand: () => { throw new Error('go not found') }, }, }) - const result = await discoverGoWorkspaceModules(root) + const result = await discoverMocked(root) expect(result).to.have.lengthOf(0) }) test('returns empty when go output is invalid JSON', async () => { const root = path.resolve('test/providers/tst_manifests/golang/go_workspace') - const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { + const { discoverGoWorkspaceModules: discoverMocked } = await esmock('../../src/providers/golang_gomodules.js', { '../../src/tools.js': { getCustom: () => null, getCustomPath: () => 'go', invokeCommand: () => Buffer.from('not json'), }, }) - const result = await discoverGoWorkspaceModules(root) + const result = await discoverMocked(root) expect(result).to.have.lengthOf(0) }) - test('returns empty when Use array is empty', async () => { - const root = path.resolve('test/providers/tst_manifests/golang/go_workspace') - const goWorkJson = { Go: { Version: '1.22' }, Use: [] } - const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { - '../../src/tools.js': { - getCustom: () => null, - getCustomPath: () => 'go', - invokeCommand: () => Buffer.from(JSON.stringify(goWorkJson)), - }, - }) + test('returns empty when Use is null (no use directives)', async () => { + const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_empty') const result = await discoverGoWorkspaceModules(root) expect(result).to.have.lengthOf(0) }) test('applies ignore patterns to discovered modules', async () => { const root = path.resolve('test/providers/tst_manifests/golang/go_workspace_nested') - const goWorkJson = { - Go: { Version: '1.22' }, - Use: [ - { DiskPath: './libs/core' }, - { DiskPath: './libs/util' }, - ], - } - const { discoverGoWorkspaceModules } = await esmock('../../src/providers/golang_gomodules.js', { - '../../src/tools.js': { - getCustom: () => null, - getCustomPath: () => 'go', - invokeCommand: () => Buffer.from(JSON.stringify(goWorkJson)), - }, - }) const result = await discoverGoWorkspaceModules(root, { workspaceDiscoveryIgnore: ['**/util/**'], })