@shazhou/proman-core 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/LICENSE +18 -0
- package/dist/commands/bump.d.ts +13 -0
- package/dist/commands/bump.d.ts.map +1 -0
- package/dist/commands/bump.js +115 -0
- package/dist/commands/deploy.d.ts +9 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +42 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +175 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +262 -0
- package/dist/commands/link.d.ts +19 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +155 -0
- package/dist/commands/publish.d.ts +18 -0
- package/dist/commands/publish.d.ts.map +1 -0
- package/dist/commands/publish.js +125 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +2 -0
- package/dist/config/load-config.d.ts +6 -0
- package/dist/config/load-config.d.ts.map +1 -0
- package/dist/config/load-config.js +29 -0
- package/dist/config/types.d.ts +17 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +1 -0
- package/dist/config/validate-config.d.ts +7 -0
- package/dist/config/validate-config.d.ts.map +1 -0
- package/dist/config/validate-config.js +72 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/utils/changeset.d.ts +16 -0
- package/dist/utils/changeset.d.ts.map +1 -0
- package/dist/utils/changeset.js +80 -0
- package/dist/utils/fingerprint.d.ts +38 -0
- package/dist/utils/fingerprint.d.ts.map +1 -0
- package/dist/utils/fingerprint.js +182 -0
- package/dist/utils/git.d.ts +23 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +105 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/npm.d.ts +30 -0
- package/dist/utils/npm.d.ts.map +1 -0
- package/dist/utils/npm.js +85 -0
- package/dist/utils/smoke-test.d.ts +7 -0
- package/dist/utils/smoke-test.d.ts.map +1 -0
- package/dist/utils/smoke-test.js +59 -0
- package/dist/utils/version.d.ts +5 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +36 -0
- package/dist/utils/workspace.d.ts +21 -0
- package/dist/utils/workspace.d.ts.map +1 -0
- package/dist/utils/workspace.js +73 -0
- package/package.json +45 -0
- package/src/commands/bump.ts +131 -0
- package/src/commands/deploy.ts +52 -0
- package/src/commands/dev.ts +214 -0
- package/src/commands/index.ts +7 -0
- package/src/commands/init.integration.test.ts +59 -0
- package/src/commands/init.test.ts +179 -0
- package/src/commands/init.ts +290 -0
- package/src/commands/link.ts +195 -0
- package/src/commands/publish.ts +168 -0
- package/src/config/index.ts +8 -0
- package/src/config/load-config.ts +33 -0
- package/src/config/types.ts +19 -0
- package/src/config/validate-config.ts +81 -0
- package/src/index.ts +29 -0
- package/src/utils/changeset.ts +98 -0
- package/src/utils/fingerprint.ts +199 -0
- package/src/utils/git.ts +119 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/npm.ts +110 -0
- package/src/utils/smoke-test.ts +79 -0
- package/src/utils/version.ts +41 -0
- package/src/utils/workspace.ts +94 -0
- package/tests/build-fingerprint-integration.test.ts +403 -0
- package/tests/bump.test.ts +261 -0
- package/tests/changeset.test.ts +147 -0
- package/tests/deploy.test.ts +98 -0
- package/tests/dev.test.ts +756 -0
- package/tests/fingerprint.test.ts +316 -0
- package/tests/fixtures/api-only/packages/api/.gitkeep +0 -0
- package/tests/fixtures/api-only/proman.yaml +4 -0
- package/tests/fixtures/bad-packages/proman.yaml +1 -0
- package/tests/fixtures/bun-project/packages/a/.gitkeep +0 -0
- package/tests/fixtures/bun-project/proman.yaml +4 -0
- package/tests/fixtures/defaults/proman.yaml +3 -0
- package/tests/fixtures/no-deployable/packages/core/.gitkeep +0 -0
- package/tests/fixtures/no-deployable/packages/mycli/.gitkeep +0 -0
- package/tests/fixtures/no-deployable/proman.yaml +7 -0
- package/tests/fixtures/node-runtime/packages/a/package.json +5 -0
- package/tests/fixtures/node-runtime/proman.yaml +3 -0
- package/tests/fixtures/pnpm-project/packages/a/package.json +1 -0
- package/tests/fixtures/pnpm-project/pnpm-lock.yaml +0 -0
- package/tests/fixtures/pnpm-project/proman.yaml +3 -0
- package/tests/fixtures/typed/packages/api/.gitkeep +0 -0
- package/tests/fixtures/typed/packages/core/.gitkeep +0 -0
- package/tests/fixtures/typed/packages/dashboard/.gitkeep +0 -0
- package/tests/fixtures/typed/packages/mycli/.gitkeep +0 -0
- package/tests/fixtures/typed/proman.yaml +13 -0
- package/tests/fixtures/valid/packages/cli/package.json +5 -0
- package/tests/fixtures/valid/packages/core/package.json +5 -0
- package/tests/fixtures/valid/packages/fs/package.json +5 -0
- package/tests/fixtures/valid/proman.yaml +13 -0
- package/tests/fixtures/webui-only/packages/dashboard/.gitkeep +0 -0
- package/tests/fixtures/webui-only/proman.yaml +4 -0
- package/tests/link.test.ts +419 -0
- package/tests/load-config.test.ts +44 -0
- package/tests/npm.test.ts +199 -0
- package/tests/publish.test.ts +599 -0
- package/tests/smoke-test.test.ts +211 -0
- package/tests/validate-config.test.ts +67 -0
- package/tests/version.test.ts +86 -0
- package/tests/workflow-schema.test.ts +72 -0
- package/tests/workspace.test.ts +160 -0
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { dirname, join, resolve } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
|
9
|
+
import { build, check, format, runTests } from '../src/commands/dev.ts'
|
|
10
|
+
|
|
11
|
+
import type { SpawnFn } from '../src/utils/npm.ts'
|
|
12
|
+
|
|
13
|
+
const FIX = (name: string) => resolve(__dirname, 'fixtures', name)
|
|
14
|
+
|
|
15
|
+
type Call = { argv: string[]; cwd: string }
|
|
16
|
+
|
|
17
|
+
function makeSpawn(code = 0, stdout = '', stderr = '') {
|
|
18
|
+
const calls: Call[] = []
|
|
19
|
+
const fn: SpawnFn = async (argv, cwd) => {
|
|
20
|
+
calls.push({ argv, cwd })
|
|
21
|
+
return { code, stdout, stderr }
|
|
22
|
+
}
|
|
23
|
+
return { spawn: fn, calls }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Check that argv contains `pm exec bin` pattern */
|
|
27
|
+
function expectExec(argv: string[], bin: string, args: string[]) {
|
|
28
|
+
const binIdx = argv.indexOf(bin)
|
|
29
|
+
expect(binIdx).toBeGreaterThanOrEqual(0)
|
|
30
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
|
+
expect(argv[binIdx + 1 + i]).toBe(args[i])
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('build command', () => {
|
|
36
|
+
test('C1: dispatches tsc --build per package in order', async () => {
|
|
37
|
+
const { spawn, calls } = makeSpawn()
|
|
38
|
+
await build({ cwd: FIX('valid'), spawn })
|
|
39
|
+
expect(calls).toHaveLength(3) // core, fs, cli
|
|
40
|
+
for (const c of calls) {
|
|
41
|
+
expect(c.argv).toEqual(['pnpm', 'exec', 'tsc', '--build'])
|
|
42
|
+
expectExec(c.argv, 'tsc', ['--build'])
|
|
43
|
+
}
|
|
44
|
+
expect(calls[0]?.cwd).toBe(resolve(FIX('valid'), 'packages/core'))
|
|
45
|
+
expect(calls[1]?.cwd).toBe(resolve(FIX('valid'), 'packages/fs'))
|
|
46
|
+
expect(calls[2]?.cwd).toBe(resolve(FIX('valid'), 'packages/cli'))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('C1b: node-runtime uses pnpm exec tsc --build', async () => {
|
|
50
|
+
const { spawn, calls } = makeSpawn()
|
|
51
|
+
await build({ cwd: FIX('node-runtime'), spawn })
|
|
52
|
+
expect(calls).toHaveLength(1)
|
|
53
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'exec', 'tsc', '--build'])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('C1c: pnpm project uses pnpm exec tsc --build', async () => {
|
|
57
|
+
const { spawn, calls } = makeSpawn()
|
|
58
|
+
await build({ cwd: FIX('pnpm-project'), spawn })
|
|
59
|
+
expect(calls).toHaveLength(1)
|
|
60
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'exec', 'tsc', '--build'])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('C1d: typed fixture dispatches by type in declared order', async () => {
|
|
64
|
+
const { spawn, calls } = makeSpawn()
|
|
65
|
+
await build({ cwd: FIX('typed'), spawn })
|
|
66
|
+
expect(calls).toHaveLength(4)
|
|
67
|
+
// lib: tsc --build
|
|
68
|
+
expectExec(calls[0]?.argv, 'tsc', ['--build'])
|
|
69
|
+
expect(calls[0]?.cwd).toBe(resolve(FIX('typed'), 'packages/core'))
|
|
70
|
+
// cli: pnpm run build
|
|
71
|
+
expect(calls[1]?.argv).toEqual(['pnpm', 'run', 'build'])
|
|
72
|
+
expect(calls[1]?.cwd).toBe(resolve(FIX('typed'), 'packages/mycli'))
|
|
73
|
+
// webui: vite build
|
|
74
|
+
expectExec(calls[2]?.argv, 'vite', ['build'])
|
|
75
|
+
expect(calls[2]?.cwd).toBe(resolve(FIX('typed'), 'packages/dashboard'))
|
|
76
|
+
// api: tsc --build
|
|
77
|
+
expectExec(calls[3]?.argv, 'tsc', ['--build'])
|
|
78
|
+
expect(calls[3]?.cwd).toBe(resolve(FIX('typed'), 'packages/api'))
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('C-bin: webui uses pnpm exec vite', async () => {
|
|
82
|
+
const { spawn, calls } = makeSpawn()
|
|
83
|
+
await build({ cwd: FIX('webui-only'), spawn })
|
|
84
|
+
expect(calls).toHaveLength(1)
|
|
85
|
+
expectExec(calls[0]?.argv, 'vite', ['build'])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('C-pnpm: uses pnpm exec', async () => {
|
|
89
|
+
const { spawn, calls } = makeSpawn()
|
|
90
|
+
await build({ cwd: FIX('valid'), spawn })
|
|
91
|
+
expect(calls[0]?.argv[0]).toBe('pnpm')
|
|
92
|
+
expect(calls[0]?.argv[1]).toBe('exec')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('C6: build throws on non-zero exit', async () => {
|
|
96
|
+
const { spawn } = makeSpawn(1, '', 'build error')
|
|
97
|
+
await expect(build({ cwd: FIX('valid'), spawn })).rejects.toThrow()
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('test command', () => {
|
|
102
|
+
test('C2: invokes pnpm exec vitest run', async () => {
|
|
103
|
+
const { spawn, calls } = makeSpawn()
|
|
104
|
+
await runTests({ cwd: FIX('valid'), spawn })
|
|
105
|
+
expect(calls).toHaveLength(1)
|
|
106
|
+
const { argv, cwd } = calls[0] as Call
|
|
107
|
+
expect(argv).toEqual(['pnpm', 'exec', 'vitest', 'run'])
|
|
108
|
+
expect(cwd).toBe(FIX('valid'))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('C3: node-runtime invokes pnpm exec vitest run', async () => {
|
|
112
|
+
const { spawn, calls } = makeSpawn()
|
|
113
|
+
await runTests({ cwd: FIX('node-runtime'), spawn })
|
|
114
|
+
expect(calls).toHaveLength(1)
|
|
115
|
+
const { argv } = calls[0] as Call
|
|
116
|
+
expect(argv).toEqual(['pnpm', 'exec', 'vitest', 'run'])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('C3b: pnpm project invokes pnpm exec vitest run', async () => {
|
|
120
|
+
const { spawn, calls } = makeSpawn()
|
|
121
|
+
await runTests({ cwd: FIX('pnpm-project'), spawn })
|
|
122
|
+
expect(calls).toHaveLength(1)
|
|
123
|
+
const { argv } = calls[0] as Call
|
|
124
|
+
expect(argv).toEqual(['pnpm', 'exec', 'vitest', 'run'])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('C6: test throws on non-zero exit', async () => {
|
|
128
|
+
const { spawn } = makeSpawn(1, '', 'fail')
|
|
129
|
+
await expect(runTests({ cwd: FIX('valid'), spawn })).rejects.toThrow()
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('check command', () => {
|
|
134
|
+
test('C4: invokes pnpm exec biome check .', async () => {
|
|
135
|
+
const { spawn, calls } = makeSpawn()
|
|
136
|
+
await check({ cwd: FIX('node-runtime'), spawn })
|
|
137
|
+
expect(calls).toHaveLength(1)
|
|
138
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'exec', 'biome', 'check', '.'])
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('C4b: valid fixture invokes pnpm exec biome check .', async () => {
|
|
142
|
+
const { spawn, calls } = makeSpawn()
|
|
143
|
+
await check({ cwd: FIX('valid'), spawn })
|
|
144
|
+
expect(calls).toHaveLength(1)
|
|
145
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'exec', 'biome', 'check', '.'])
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('C4c: pnpm project invokes pnpm exec biome check .', async () => {
|
|
149
|
+
const { spawn, calls } = makeSpawn()
|
|
150
|
+
await check({ cwd: FIX('pnpm-project'), spawn })
|
|
151
|
+
expect(calls).toHaveLength(1)
|
|
152
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'exec', 'biome', 'check', '.'])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('C6: check throws on non-zero exit', async () => {
|
|
156
|
+
const { spawn } = makeSpawn(1, '', 'fail')
|
|
157
|
+
await expect(check({ cwd: FIX('valid'), spawn })).rejects.toThrow()
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('check — workflow validation', () => {
|
|
162
|
+
let tmpDir: string
|
|
163
|
+
|
|
164
|
+
function writeCheckFixture(opts: {
|
|
165
|
+
workflows?: Record<string, string>
|
|
166
|
+
uwfInstalled?: boolean
|
|
167
|
+
uwfExitCode?: number
|
|
168
|
+
uwfStderr?: string
|
|
169
|
+
}): { cwd: string; spawn: SpawnFn; calls: Call[] } {
|
|
170
|
+
tmpDir = resolve(
|
|
171
|
+
tmpdir(),
|
|
172
|
+
`proman-wf-check-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
173
|
+
)
|
|
174
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
175
|
+
|
|
176
|
+
// minimal proman.yaml
|
|
177
|
+
writeFileSync(
|
|
178
|
+
join(tmpDir, 'proman.yaml'),
|
|
179
|
+
'packages:\n - name: "@test/core"\n path: packages/core\n type: lib\n',
|
|
180
|
+
)
|
|
181
|
+
mkdirSync(join(tmpDir, 'packages/core/src'), { recursive: true })
|
|
182
|
+
writeFileSync(join(tmpDir, 'packages/core/src/index.ts'), 'export const x = 1')
|
|
183
|
+
writeFileSync(join(tmpDir, 'packages/core/package.json'), '{}')
|
|
184
|
+
|
|
185
|
+
// .workflows/ files
|
|
186
|
+
if (opts.workflows) {
|
|
187
|
+
mkdirSync(join(tmpDir, '.workflows'), { recursive: true })
|
|
188
|
+
for (const [name, content] of Object.entries(opts.workflows)) {
|
|
189
|
+
writeFileSync(join(tmpDir, '.workflows', name), content)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const calls: Call[] = []
|
|
194
|
+
const uwfInstalled = opts.uwfInstalled ?? true
|
|
195
|
+
const uwfExitCode = opts.uwfExitCode ?? 0
|
|
196
|
+
const uwfStderr = opts.uwfStderr ?? ''
|
|
197
|
+
|
|
198
|
+
const spawn: SpawnFn = async (argv, cwd) => {
|
|
199
|
+
calls.push({ argv, cwd })
|
|
200
|
+
// biome check always passes
|
|
201
|
+
if (argv.includes('biome')) return { code: 0, stdout: '', stderr: '' }
|
|
202
|
+
// which uwf
|
|
203
|
+
if (argv[0] === 'which' && argv[1] === 'uwf') {
|
|
204
|
+
return { code: uwfInstalled ? 0 : 1, stdout: '', stderr: '' }
|
|
205
|
+
}
|
|
206
|
+
// uwf workflow validate
|
|
207
|
+
if (argv[0] === 'uwf' && argv[1] === 'workflow' && argv[2] === 'validate') {
|
|
208
|
+
return { code: uwfExitCode, stdout: uwfExitCode === 0 ? '✓ valid' : '', stderr: uwfStderr }
|
|
209
|
+
}
|
|
210
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { cwd: tmpDir, spawn, calls }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
afterEach(() => {
|
|
217
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true })
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test('WF1: validates workflow files with uwf workflow validate', async () => {
|
|
221
|
+
const { cwd, spawn, calls } = writeCheckFixture({
|
|
222
|
+
workflows: { 'solve-issue.yaml': 'version: 1\nname: solve-issue\n' },
|
|
223
|
+
})
|
|
224
|
+
await check({ cwd, spawn })
|
|
225
|
+
|
|
226
|
+
const uwfCalls = calls.filter((c) => c.argv[0] === 'uwf')
|
|
227
|
+
expect(uwfCalls).toHaveLength(1)
|
|
228
|
+
expect(uwfCalls[0]?.argv[2]).toBe('validate')
|
|
229
|
+
expect(uwfCalls[0]?.argv[3]).toContain('solve-issue.yaml')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('WF2: validates multiple workflow files', async () => {
|
|
233
|
+
const { cwd, spawn, calls } = writeCheckFixture({
|
|
234
|
+
workflows: {
|
|
235
|
+
'solve-issue.yaml': 'version: 1\nname: solve-issue\n',
|
|
236
|
+
'review-pr.yaml': 'version: 1\nname: review-pr\n',
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
await check({ cwd, spawn })
|
|
240
|
+
|
|
241
|
+
const uwfCalls = calls.filter((c) => c.argv[0] === 'uwf')
|
|
242
|
+
expect(uwfCalls).toHaveLength(2)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('WF3: skips with warning when uwf is not installed', async () => {
|
|
246
|
+
const { cwd, spawn, calls } = writeCheckFixture({
|
|
247
|
+
workflows: { 'solve-issue.yaml': 'version: 1\nname: solve-issue\n' },
|
|
248
|
+
uwfInstalled: false,
|
|
249
|
+
})
|
|
250
|
+
await check({ cwd, spawn })
|
|
251
|
+
|
|
252
|
+
const uwfValidateCalls = calls.filter((c) => c.argv[0] === 'uwf' && c.argv[1] === 'workflow')
|
|
253
|
+
expect(uwfValidateCalls).toHaveLength(0)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('WF4: throws when validation fails', async () => {
|
|
257
|
+
const { cwd, spawn } = writeCheckFixture({
|
|
258
|
+
workflows: { 'bad.yaml': 'invalid' },
|
|
259
|
+
uwfExitCode: 1,
|
|
260
|
+
uwfStderr: 'missing required field: name',
|
|
261
|
+
})
|
|
262
|
+
await expect(check({ cwd, spawn })).rejects.toThrow('Workflow validation failed')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test('WF5: no .workflows directory — skips silently', async () => {
|
|
266
|
+
const { cwd, spawn, calls } = writeCheckFixture({})
|
|
267
|
+
await check({ cwd, spawn })
|
|
268
|
+
|
|
269
|
+
const uwfCalls = calls.filter((c) => c.argv[0] === 'uwf' || c.argv[0] === 'which')
|
|
270
|
+
expect(uwfCalls).toHaveLength(0)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
test('WF6: ignores non-yaml files in .workflows/', async () => {
|
|
274
|
+
const { cwd, spawn, calls } = writeCheckFixture({
|
|
275
|
+
workflows: { 'README.md': '# workflows' },
|
|
276
|
+
})
|
|
277
|
+
await check({ cwd, spawn })
|
|
278
|
+
|
|
279
|
+
const uwfCalls = calls.filter((c) => c.argv[0] === 'uwf' || c.argv[0] === 'which')
|
|
280
|
+
expect(uwfCalls).toHaveLength(0)
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
describe('format command', () => {
|
|
285
|
+
test('C5: invokes pnpm exec biome format --write .', async () => {
|
|
286
|
+
const { spawn, calls } = makeSpawn()
|
|
287
|
+
await format({ cwd: FIX('node-runtime'), spawn })
|
|
288
|
+
expect(calls).toHaveLength(1)
|
|
289
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'exec', 'biome', 'format', '--write', '.'])
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('C6: format throws on non-zero exit', async () => {
|
|
293
|
+
const { spawn } = makeSpawn(1, '', 'fail')
|
|
294
|
+
await expect(format({ cwd: FIX('valid'), spawn })).rejects.toThrow()
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
// ── chmod +x bin entries after build ────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
describe('build — chmod +x bin entries', () => {
|
|
301
|
+
let tmpDir: string
|
|
302
|
+
|
|
303
|
+
beforeEach(() => {
|
|
304
|
+
tmpDir = resolve(tmpdir(), `proman-chmod-test-${Date.now()}`)
|
|
305
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
afterEach(() => {
|
|
309
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
function writeTmpProject(bins: Record<string, string> | string | undefined): {
|
|
313
|
+
cwd: string
|
|
314
|
+
spawn: SpawnFn
|
|
315
|
+
} {
|
|
316
|
+
const pkgDir = join(tmpDir, 'packages', 'mycli')
|
|
317
|
+
const distDir = join(pkgDir, 'dist')
|
|
318
|
+
mkdirSync(distDir, { recursive: true })
|
|
319
|
+
|
|
320
|
+
// proman.yaml
|
|
321
|
+
writeFileSync(
|
|
322
|
+
join(tmpDir, 'proman.yaml'),
|
|
323
|
+
'packages:\n - name: "@test/cli"\n path: packages/mycli\n type: cli\n',
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
// package.json with bin
|
|
327
|
+
const pkgJson: Record<string, unknown> = { name: '@test/cli', version: '1.0.0' }
|
|
328
|
+
if (bins !== undefined) pkgJson.bin = bins
|
|
329
|
+
writeFileSync(join(pkgDir, 'package.json'), JSON.stringify(pkgJson))
|
|
330
|
+
|
|
331
|
+
// Mock spawn that recreates dist/cli.js with 644 (simulates tsc output)
|
|
332
|
+
const spawn: SpawnFn = async (_argv, cwd) => {
|
|
333
|
+
const out = join(cwd, 'dist')
|
|
334
|
+
mkdirSync(out, { recursive: true })
|
|
335
|
+
writeFileSync(join(out, 'cli.js'), '#!/usr/bin/env node\n')
|
|
336
|
+
chmodSync(join(out, 'cli.js'), 0o644)
|
|
337
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { cwd: tmpDir, spawn }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
test('B1: bin object — chmod +x applied after build', async () => {
|
|
344
|
+
const { cwd, spawn } = writeTmpProject({ mycli: './dist/cli.js' })
|
|
345
|
+
await build({ cwd, spawn })
|
|
346
|
+
|
|
347
|
+
const mode = statSync(join(cwd, 'packages/mycli/dist/cli.js')).mode & 0o777
|
|
348
|
+
expect(mode).toBe(0o755)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('B2: bin string — chmod +x applied after build', async () => {
|
|
352
|
+
const { cwd, spawn } = writeTmpProject('./dist/cli.js')
|
|
353
|
+
await build({ cwd, spawn })
|
|
354
|
+
|
|
355
|
+
const mode = statSync(join(cwd, 'packages/mycli/dist/cli.js')).mode & 0o777
|
|
356
|
+
expect(mode).toBe(0o755)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test('B3: no bin field — no crash', async () => {
|
|
360
|
+
const { cwd, spawn } = writeTmpProject(undefined)
|
|
361
|
+
await build({ cwd, spawn })
|
|
362
|
+
// Should not throw
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('B4: bin points to missing file — no crash', async () => {
|
|
366
|
+
const { cwd, spawn } = writeTmpProject({ mycli: './dist/nonexistent.js' })
|
|
367
|
+
await build({ cwd, spawn })
|
|
368
|
+
// Should not throw
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
// ── fingerprint skip ────────────────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
describe('build — fingerprint skip', () => {
|
|
375
|
+
let tmpDir: string
|
|
376
|
+
|
|
377
|
+
function writeMonorepoFixture(): string {
|
|
378
|
+
tmpDir = resolve(
|
|
379
|
+
tmpdir(),
|
|
380
|
+
`proman-fp-build-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
381
|
+
)
|
|
382
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
383
|
+
|
|
384
|
+
// proman.yaml
|
|
385
|
+
writeFileSync(
|
|
386
|
+
join(tmpDir, 'proman.yaml'),
|
|
387
|
+
[
|
|
388
|
+
'packages:',
|
|
389
|
+
' - name: "@test/core"',
|
|
390
|
+
' path: packages/core',
|
|
391
|
+
' type: lib',
|
|
392
|
+
' - name: "@test/fs"',
|
|
393
|
+
' path: packages/fs',
|
|
394
|
+
' type: lib',
|
|
395
|
+
' - name: "@test/cli"',
|
|
396
|
+
' path: packages/cli',
|
|
397
|
+
' type: cli',
|
|
398
|
+
].join('\n'),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
// core (no deps)
|
|
402
|
+
mkdirSync(join(tmpDir, 'packages/core/src'), { recursive: true })
|
|
403
|
+
writeFileSync(join(tmpDir, 'packages/core/src/index.ts'), 'export const x = 1')
|
|
404
|
+
writeFileSync(
|
|
405
|
+
join(tmpDir, 'packages/core/package.json'),
|
|
406
|
+
JSON.stringify({ name: '@test/core', version: '1.0.0' }),
|
|
407
|
+
)
|
|
408
|
+
writeFileSync(join(tmpDir, 'packages/core/tsconfig.json'), '{}')
|
|
409
|
+
|
|
410
|
+
// fs (depends on core)
|
|
411
|
+
mkdirSync(join(tmpDir, 'packages/fs/src'), { recursive: true })
|
|
412
|
+
writeFileSync(join(tmpDir, 'packages/fs/src/index.ts'), 'export const y = 2')
|
|
413
|
+
writeFileSync(
|
|
414
|
+
join(tmpDir, 'packages/fs/package.json'),
|
|
415
|
+
JSON.stringify({
|
|
416
|
+
name: '@test/fs',
|
|
417
|
+
version: '1.0.0',
|
|
418
|
+
dependencies: { '@test/core': 'workspace:*' },
|
|
419
|
+
}),
|
|
420
|
+
)
|
|
421
|
+
writeFileSync(join(tmpDir, 'packages/fs/tsconfig.json'), '{}')
|
|
422
|
+
|
|
423
|
+
// cli (depends on fs)
|
|
424
|
+
mkdirSync(join(tmpDir, 'packages/cli/src'), { recursive: true })
|
|
425
|
+
writeFileSync(join(tmpDir, 'packages/cli/src/index.ts'), 'export const z = 3')
|
|
426
|
+
writeFileSync(
|
|
427
|
+
join(tmpDir, 'packages/cli/package.json'),
|
|
428
|
+
JSON.stringify({
|
|
429
|
+
name: '@test/cli',
|
|
430
|
+
version: '1.0.0',
|
|
431
|
+
dependencies: { '@test/fs': 'workspace:*' },
|
|
432
|
+
}),
|
|
433
|
+
)
|
|
434
|
+
writeFileSync(join(tmpDir, 'packages/cli/tsconfig.json'), '{}')
|
|
435
|
+
|
|
436
|
+
return tmpDir
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
afterEach(() => {
|
|
440
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true })
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
test('FP-B1: first run — no stored fingerprint → runs build, writes fingerprint', async () => {
|
|
444
|
+
const cwd = writeMonorepoFixture()
|
|
445
|
+
const { spawn, calls } = makeSpawn()
|
|
446
|
+
|
|
447
|
+
await build({ cwd, spawn, force: false })
|
|
448
|
+
|
|
449
|
+
// build ran for all 3 packages
|
|
450
|
+
expect(calls.length).toBe(3)
|
|
451
|
+
// fingerprint files written inside each package's dist folder
|
|
452
|
+
expect(existsSync(join(cwd, 'packages/core/dist/.build-fingerprint'))).toBe(true)
|
|
453
|
+
expect(existsSync(join(cwd, 'packages/fs/dist/.build-fingerprint'))).toBe(true)
|
|
454
|
+
expect(existsSync(join(cwd, 'packages/cli/dist/.build-fingerprint'))).toBe(true)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
test('FP-B2: second run — fingerprint matches → skips build', async () => {
|
|
458
|
+
const cwd = writeMonorepoFixture()
|
|
459
|
+
const { spawn, calls } = makeSpawn()
|
|
460
|
+
|
|
461
|
+
await build({ cwd, spawn, force: false })
|
|
462
|
+
expect(calls.length).toBe(3)
|
|
463
|
+
|
|
464
|
+
// Second run — should skip all
|
|
465
|
+
const { spawn: spawn2, calls: calls2 } = makeSpawn()
|
|
466
|
+
await build({ cwd, spawn: spawn2, force: false })
|
|
467
|
+
expect(calls2.length).toBe(0)
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
test('FP-B3: file changed — fingerprint mismatches → runs build', async () => {
|
|
471
|
+
const cwd = writeMonorepoFixture()
|
|
472
|
+
const { spawn } = makeSpawn()
|
|
473
|
+
await build({ cwd, spawn, force: false })
|
|
474
|
+
|
|
475
|
+
// Modify core's source
|
|
476
|
+
writeFileSync(join(cwd, 'packages/core/src/index.ts'), 'export const x = 999')
|
|
477
|
+
|
|
478
|
+
const { spawn: spawn2, calls: calls2 } = makeSpawn()
|
|
479
|
+
await build({ cwd, spawn: spawn2, force: false })
|
|
480
|
+
// All 3 should re-run (core changed, fs/cli depend on core)
|
|
481
|
+
expect(calls2.length).toBe(3)
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
test('FP-B4: force=true — runs even when fingerprint matches', async () => {
|
|
485
|
+
const cwd = writeMonorepoFixture()
|
|
486
|
+
const { spawn } = makeSpawn()
|
|
487
|
+
await build({ cwd, spawn, force: false })
|
|
488
|
+
|
|
489
|
+
const { spawn: spawn2, calls: calls2 } = makeSpawn()
|
|
490
|
+
await build({ cwd, spawn: spawn2, force: true })
|
|
491
|
+
expect(calls2.length).toBe(3)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
test('FP-B5: build failure — does NOT write fingerprint', async () => {
|
|
495
|
+
const cwd = writeMonorepoFixture()
|
|
496
|
+
const { spawn } = makeSpawn(1, '', 'build error')
|
|
497
|
+
|
|
498
|
+
await expect(build({ cwd, spawn, force: false })).rejects.toThrow()
|
|
499
|
+
|
|
500
|
+
// No fingerprint files should exist
|
|
501
|
+
expect(existsSync(join(cwd, '.proman/build/@test-core.fingerprint'))).toBe(false)
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
test('FP-B6: dependency propagation — core change re-runs fs and cli', async () => {
|
|
505
|
+
const cwd = writeMonorepoFixture()
|
|
506
|
+
const { spawn } = makeSpawn()
|
|
507
|
+
await build({ cwd, spawn, force: false })
|
|
508
|
+
|
|
509
|
+
// Modify only core
|
|
510
|
+
writeFileSync(join(cwd, 'packages/core/src/index.ts'), 'export const x = 777')
|
|
511
|
+
|
|
512
|
+
const { spawn: spawn2, calls: calls2 } = makeSpawn()
|
|
513
|
+
await build({ cwd, spawn: spawn2, force: false })
|
|
514
|
+
|
|
515
|
+
// All 3 re-run because core → fs → cli dependency chain
|
|
516
|
+
expect(calls2.length).toBe(3)
|
|
517
|
+
expect(calls2[0]?.cwd).toBe(resolve(cwd, 'packages/core'))
|
|
518
|
+
expect(calls2[1]?.cwd).toBe(resolve(cwd, 'packages/fs'))
|
|
519
|
+
expect(calls2[2]?.cwd).toBe(resolve(cwd, 'packages/cli'))
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
test('FP-B7: CI env — runs build even when fingerprint matches', async () => {
|
|
523
|
+
const cwd = writeMonorepoFixture()
|
|
524
|
+
const { spawn } = makeSpawn()
|
|
525
|
+
await build({ cwd, spawn, force: false })
|
|
526
|
+
|
|
527
|
+
// Simulate CI behavior: cli.ts passes force=true when CI=true
|
|
528
|
+
const { spawn: spawn2, calls: calls2 } = makeSpawn()
|
|
529
|
+
await build({ cwd, spawn: spawn2, force: true })
|
|
530
|
+
expect(calls2.length).toBe(3)
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
test('FP-B8: partial skip — only stale packages rebuild', async () => {
|
|
534
|
+
// Monorepo with 2 independent packages (no cross-deps)
|
|
535
|
+
const dir = resolve(
|
|
536
|
+
tmpdir(),
|
|
537
|
+
`proman-fp-partial-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
538
|
+
)
|
|
539
|
+
mkdirSync(dir, { recursive: true })
|
|
540
|
+
tmpDir = dir
|
|
541
|
+
|
|
542
|
+
writeFileSync(
|
|
543
|
+
join(dir, 'proman.yaml'),
|
|
544
|
+
[
|
|
545
|
+
'packages:',
|
|
546
|
+
' - name: "@test/alpha"',
|
|
547
|
+
' path: packages/alpha',
|
|
548
|
+
' type: lib',
|
|
549
|
+
' - name: "@test/beta"',
|
|
550
|
+
' path: packages/beta',
|
|
551
|
+
' type: lib',
|
|
552
|
+
].join('\n'),
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
// alpha (no deps)
|
|
556
|
+
mkdirSync(join(dir, 'packages/alpha/src'), { recursive: true })
|
|
557
|
+
writeFileSync(join(dir, 'packages/alpha/src/index.ts'), 'export const a = 1')
|
|
558
|
+
writeFileSync(
|
|
559
|
+
join(dir, 'packages/alpha/package.json'),
|
|
560
|
+
JSON.stringify({ name: '@test/alpha', version: '1.0.0' }),
|
|
561
|
+
)
|
|
562
|
+
writeFileSync(join(dir, 'packages/alpha/tsconfig.json'), '{}')
|
|
563
|
+
|
|
564
|
+
// beta (no deps)
|
|
565
|
+
mkdirSync(join(dir, 'packages/beta/src'), { recursive: true })
|
|
566
|
+
writeFileSync(join(dir, 'packages/beta/src/index.ts'), 'export const b = 1')
|
|
567
|
+
writeFileSync(
|
|
568
|
+
join(dir, 'packages/beta/package.json'),
|
|
569
|
+
JSON.stringify({ name: '@test/beta', version: '1.0.0' }),
|
|
570
|
+
)
|
|
571
|
+
writeFileSync(join(dir, 'packages/beta/tsconfig.json'), '{}')
|
|
572
|
+
|
|
573
|
+
// First run — both build
|
|
574
|
+
const { spawn: s1, calls: c1 } = makeSpawn()
|
|
575
|
+
await build({ cwd: dir, spawn: s1, force: false })
|
|
576
|
+
expect(c1.length).toBe(2)
|
|
577
|
+
|
|
578
|
+
// Modify only beta
|
|
579
|
+
writeFileSync(join(dir, 'packages/beta/src/index.ts'), 'export const b = 999')
|
|
580
|
+
|
|
581
|
+
// Second run — only beta should rebuild
|
|
582
|
+
const { spawn: s2, calls: c2 } = makeSpawn()
|
|
583
|
+
await build({ cwd: dir, spawn: s2, force: false })
|
|
584
|
+
expect(c2.length).toBe(1)
|
|
585
|
+
expect(c2[0]?.cwd).toBe(resolve(dir, 'packages/beta'))
|
|
586
|
+
})
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
describe('test — fingerprint skip', () => {
|
|
590
|
+
let tmpDir: string
|
|
591
|
+
|
|
592
|
+
function writeRootFixture(): string {
|
|
593
|
+
tmpDir = resolve(
|
|
594
|
+
tmpdir(),
|
|
595
|
+
`proman-fp-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
596
|
+
)
|
|
597
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
598
|
+
|
|
599
|
+
writeFileSync(
|
|
600
|
+
join(tmpDir, 'proman.yaml'),
|
|
601
|
+
'packages:\n - name: "@test/core"\n path: packages/core\n type: lib\n',
|
|
602
|
+
)
|
|
603
|
+
mkdirSync(join(tmpDir, 'packages/core/src'), { recursive: true })
|
|
604
|
+
writeFileSync(join(tmpDir, 'packages/core/src/index.ts'), 'export const x = 1')
|
|
605
|
+
writeFileSync(join(tmpDir, 'packages/core/package.json'), '{}')
|
|
606
|
+
mkdirSync(join(tmpDir, 'src'), { recursive: true })
|
|
607
|
+
mkdirSync(join(tmpDir, 'tests'), { recursive: true })
|
|
608
|
+
writeFileSync(join(tmpDir, 'src/a.ts'), 'export const a = 1')
|
|
609
|
+
writeFileSync(join(tmpDir, 'tests/a.test.ts'), 'test("a", () => {})')
|
|
610
|
+
writeFileSync(join(tmpDir, 'package.json'), '{}')
|
|
611
|
+
return tmpDir
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
afterEach(() => {
|
|
615
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true })
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
test('FP-T1: first run → runs test, writes fingerprint', async () => {
|
|
619
|
+
const cwd = writeRootFixture()
|
|
620
|
+
const { spawn, calls } = makeSpawn()
|
|
621
|
+
|
|
622
|
+
await runTests({ cwd, spawn, force: false })
|
|
623
|
+
|
|
624
|
+
expect(calls.length).toBe(1)
|
|
625
|
+
expect(existsSync(join(cwd, '.proman/test/root.fingerprint'))).toBe(true)
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
test('FP-T2: no changes → skips test', async () => {
|
|
629
|
+
const cwd = writeRootFixture()
|
|
630
|
+
const { spawn } = makeSpawn()
|
|
631
|
+
await runTests({ cwd, spawn, force: false })
|
|
632
|
+
|
|
633
|
+
const { spawn: spawn2, calls: calls2 } = makeSpawn()
|
|
634
|
+
await runTests({ cwd, spawn: spawn2, force: false })
|
|
635
|
+
expect(calls2.length).toBe(0)
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
test('FP-T3: force=true → runs test even if cached', async () => {
|
|
639
|
+
const cwd = writeRootFixture()
|
|
640
|
+
const { spawn } = makeSpawn()
|
|
641
|
+
await runTests({ cwd, spawn, force: false })
|
|
642
|
+
|
|
643
|
+
const { spawn: spawn2, calls: calls2 } = makeSpawn()
|
|
644
|
+
await runTests({ cwd, spawn: spawn2, force: true })
|
|
645
|
+
expect(calls2.length).toBe(1)
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
test('FP-T4: test failure → fingerprint NOT written', async () => {
|
|
649
|
+
const cwd = writeRootFixture()
|
|
650
|
+
const { spawn } = makeSpawn(1, '', 'test failure')
|
|
651
|
+
|
|
652
|
+
await expect(runTests({ cwd, spawn, force: false })).rejects.toThrow()
|
|
653
|
+
expect(existsSync(join(cwd, '.proman/test/root.fingerprint'))).toBe(false)
|
|
654
|
+
})
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
describe('check — fingerprint skip', () => {
|
|
658
|
+
let tmpDir: string
|
|
659
|
+
|
|
660
|
+
function writeCheckFixture(): string {
|
|
661
|
+
tmpDir = resolve(
|
|
662
|
+
tmpdir(),
|
|
663
|
+
`proman-fp-check-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
664
|
+
)
|
|
665
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
666
|
+
|
|
667
|
+
writeFileSync(
|
|
668
|
+
join(tmpDir, 'proman.yaml'),
|
|
669
|
+
'packages:\n - name: "@test/core"\n path: packages/core\n type: lib\n',
|
|
670
|
+
)
|
|
671
|
+
mkdirSync(join(tmpDir, 'packages/core/src'), { recursive: true })
|
|
672
|
+
writeFileSync(join(tmpDir, 'packages/core/src/index.ts'), 'export const x = 1')
|
|
673
|
+
writeFileSync(join(tmpDir, 'packages/core/package.json'), '{}')
|
|
674
|
+
mkdirSync(join(tmpDir, 'src'), { recursive: true })
|
|
675
|
+
writeFileSync(join(tmpDir, 'src/a.ts'), 'export const a = 1')
|
|
676
|
+
writeFileSync(join(tmpDir, 'package.json'), '{}')
|
|
677
|
+
writeFileSync(join(tmpDir, 'biome.json'), '{}')
|
|
678
|
+
return tmpDir
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
afterEach(() => {
|
|
682
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true })
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
test('FP-C1: first run → runs check, writes fingerprint', async () => {
|
|
686
|
+
const cwd = writeCheckFixture()
|
|
687
|
+
const { spawn, calls } = makeSpawn()
|
|
688
|
+
|
|
689
|
+
await check({ cwd, spawn, force: false })
|
|
690
|
+
|
|
691
|
+
expect(calls.length).toBe(1)
|
|
692
|
+
expect(existsSync(join(cwd, '.proman/check/root.fingerprint'))).toBe(true)
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
test('FP-C2: no changes → skips check', async () => {
|
|
696
|
+
const cwd = writeCheckFixture()
|
|
697
|
+
const { spawn } = makeSpawn()
|
|
698
|
+
await check({ cwd, spawn, force: false })
|
|
699
|
+
|
|
700
|
+
const { spawn: spawn2, calls: calls2 } = makeSpawn()
|
|
701
|
+
await check({ cwd, spawn: spawn2, force: false })
|
|
702
|
+
expect(calls2.length).toBe(0)
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
test('FP-C3: force=true → runs check even if cached', async () => {
|
|
706
|
+
const cwd = writeCheckFixture()
|
|
707
|
+
const { spawn } = makeSpawn()
|
|
708
|
+
await check({ cwd, spawn, force: false })
|
|
709
|
+
|
|
710
|
+
const { spawn: spawn2, calls: calls2 } = makeSpawn()
|
|
711
|
+
await check({ cwd, spawn: spawn2, force: true })
|
|
712
|
+
expect(calls2.length).toBe(1)
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
test('FP-C4: check failure → fingerprint NOT written', async () => {
|
|
716
|
+
const cwd = writeCheckFixture()
|
|
717
|
+
const { spawn } = makeSpawn(1, '', 'check failure')
|
|
718
|
+
|
|
719
|
+
await expect(check({ cwd, spawn, force: false })).rejects.toThrow()
|
|
720
|
+
expect(existsSync(join(cwd, '.proman/check/root.fingerprint'))).toBe(false)
|
|
721
|
+
})
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
describe('format — no fingerprint', () => {
|
|
725
|
+
let tmpDir: string
|
|
726
|
+
|
|
727
|
+
afterEach(() => {
|
|
728
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true })
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
test('FP-F1: format always runs, no fingerprint directory created', async () => {
|
|
732
|
+
tmpDir = resolve(tmpdir(), `proman-fp-fmt-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
733
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
734
|
+
|
|
735
|
+
writeFileSync(
|
|
736
|
+
join(tmpDir, 'proman.yaml'),
|
|
737
|
+
'packages:\n - name: "@test/core"\n path: packages/core\n type: lib\n',
|
|
738
|
+
)
|
|
739
|
+
mkdirSync(join(tmpDir, 'packages/core/src'), { recursive: true })
|
|
740
|
+
writeFileSync(join(tmpDir, 'packages/core/src/index.ts'), 'export const x = 1')
|
|
741
|
+
writeFileSync(join(tmpDir, 'packages/core/package.json'), '{}')
|
|
742
|
+
writeFileSync(join(tmpDir, 'package.json'), '{}')
|
|
743
|
+
|
|
744
|
+
const { spawn, calls } = makeSpawn()
|
|
745
|
+
await format({ cwd: tmpDir, spawn })
|
|
746
|
+
expect(calls.length).toBe(1)
|
|
747
|
+
|
|
748
|
+
// format has no fingerprint logic — no .proman/format/ directory
|
|
749
|
+
expect(existsSync(join(tmpDir, '.proman/format'))).toBe(false)
|
|
750
|
+
|
|
751
|
+
// Second run also always runs
|
|
752
|
+
const { spawn: spawn2, calls: calls2 } = makeSpawn()
|
|
753
|
+
await format({ cwd: tmpDir, spawn: spawn2 })
|
|
754
|
+
expect(calls2.length).toBe(1)
|
|
755
|
+
})
|
|
756
|
+
})
|