@tangle-network/agent-bench 0.11.3 → 0.13.2
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 +30 -0
- package/HARNESS.md +6 -2
- package/README.md +1 -4
- package/package.json +5 -5
- package/scripts/run-package-tests.mjs +2 -2
- package/src/quant-arena/README.md +0 -144
- package/src/quant-arena/backtest.test.mts +0 -135
- package/src/quant-arena/backtest.ts +0 -218
- package/src/quant-arena/data.test.mts +0 -44
- package/src/quant-arena/data.ts +0 -141
- package/src/quant-arena/driver.test.mts +0 -253
- package/src/quant-arena/driver.ts +0 -219
- package/src/quant-arena/fixtures/data/PROVENANCE.md +0 -26
- package/src/quant-arena/fixtures/data/holdout/IDX.csv +0 -523
- package/src/quant-arena/fixtures/data/holdout/S01.csv +0 -523
- package/src/quant-arena/fixtures/data/holdout/S02.csv +0 -523
- package/src/quant-arena/fixtures/data/holdout/S03.csv +0 -523
- package/src/quant-arena/fixtures/data/holdout/S04.csv +0 -523
- package/src/quant-arena/fixtures/data/holdout/S05.csv +0 -523
- package/src/quant-arena/fixtures/data/holdout/S06.csv +0 -523
- package/src/quant-arena/fixtures/data/holdout/S07.csv +0 -523
- package/src/quant-arena/fixtures/data/holdout/S08.csv +0 -523
- package/src/quant-arena/fixtures/data/holdout/S09.csv +0 -523
- package/src/quant-arena/fixtures/data/holdout/S10.csv +0 -523
- package/src/quant-arena/fixtures/data/insample/IDX.csv +0 -2087
- package/src/quant-arena/fixtures/data/insample/S01.csv +0 -2087
- package/src/quant-arena/fixtures/data/insample/S02.csv +0 -2087
- package/src/quant-arena/fixtures/data/insample/S03.csv +0 -2087
- package/src/quant-arena/fixtures/data/insample/S04.csv +0 -2087
- package/src/quant-arena/fixtures/data/insample/S05.csv +0 -2087
- package/src/quant-arena/fixtures/data/insample/S06.csv +0 -2087
- package/src/quant-arena/fixtures/data/insample/S07.csv +0 -2087
- package/src/quant-arena/fixtures/data/insample/S08.csv +0 -2087
- package/src/quant-arena/fixtures/data/insample/S09.csv +0 -2087
- package/src/quant-arena/fixtures/data/insample/S10.csv +0 -2087
- package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +0 -16
- package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +0 -5
- package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +0 -171
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +0 -119
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +0 -119
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +0 -105
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +0 -102
- package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +0 -4
- package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +0 -2
- package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +0 -84
- package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +0 -117
- package/src/quant-arena/holdout-certify.mts +0 -206
- package/src/quant-arena/holdout-certify.test.mts +0 -82
- package/src/quant-arena/leak-audit.test.mts +0 -79
- package/src/quant-arena/leak-audit.ts +0 -95
- package/src/quant-arena/make-fixtures.mts +0 -161
- package/src/quant-arena/multiplicity.test.mts +0 -68
- package/src/quant-arena/multiplicity.ts +0 -87
- package/src/quant-arena/nautilus-certify.ts +0 -31
- package/src/quant-arena/oms.ts +0 -90
- package/src/quant-arena/profiles/quant-researcher.profile.json +0 -12
- package/src/quant-arena/python/pyproject.toml +0 -8
- package/src/quant-arena/python/uv.lock +0 -1297
- package/src/quant-arena/python/vbt-worker.py +0 -192
- package/src/quant-arena/quant-loop.mts +0 -840
- package/src/quant-arena/quant-loop.test.mts +0 -75
- package/src/quant-arena/strategies/buy-hold-index/strategy.ts +0 -11
- package/src/quant-arena/strategies/equal-weight/strategy.ts +0 -20
- package/src/quant-arena/strategies/sma-crossover/strategy.ts +0 -42
- package/src/quant-arena/types.ts +0 -133
- package/src/quant-arena/vbt-client.ts +0 -321
- package/src/quant-arena/vbt-parity.test.mts +0 -183
- package/src/quant-arena/windows.test.mts +0 -45
- package/src/quant-arena/windows.ts +0 -54
- package/src/rollout-ledger/backfill-swe-arena.mts +0 -610
- package/src/rollout-ledger/backfill-swe-arena.test.mts +0 -347
- package/src/rollout-ledger/settle-capture.mts +0 -448
- package/src/rollout-ledger/settle-capture.test.mts +0 -270
- package/src/swe-arena/activation.mts +0 -225
- package/src/swe-arena/activation.test.mts +0 -300
- package/src/swe-arena/analyze.ts +0 -211
- package/src/swe-arena/arms.ts +0 -862
- package/src/swe-arena/bootstrap-meta.mts +0 -188
- package/src/swe-arena/bootstrap-meta.test.mts +0 -51
- package/src/swe-arena/briefing.mts +0 -217
- package/src/swe-arena/briefing.test.mts +0 -179
- package/src/swe-arena/calibrate.ts +0 -217
- package/src/swe-arena/capabilities.mts +0 -76
- package/src/swe-arena/capabilities.test.mts +0 -57
- package/src/swe-arena/capacity.ts +0 -198
- package/src/swe-arena/cell-evidence.mts +0 -437
- package/src/swe-arena/cell-evidence.test.mts +0 -248
- package/src/swe-arena/diagnosis-ensemble.test.mts +0 -210
- package/src/swe-arena/diagnosis-ensemble.ts +0 -523
- package/src/swe-arena/execution.test.mts +0 -1171
- package/src/swe-arena/factory-command-container.ts +0 -284
- package/src/swe-arena/factory-judge-child.mts +0 -228
- package/src/swe-arena/factory.test.mts +0 -645
- package/src/swe-arena/fixtures/analyze.py +0 -80
- package/src/swe-arena/fixtures/excludes.txt +0 -8
- package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +0 -51
- package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +0 -29
- package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +0 -64
- package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +0 -48
- package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +0 -29
- package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +0 -48
- package/src/swe-arena/fixtures/factory/loops-28/calibration.md +0 -47
- package/src/swe-arena/fixtures/factory/loops-28/manifest.json +0 -30
- package/src/swe-arena/fixtures/factory/loops-28/spec.md +0 -50
- package/src/swe-arena/fixtures/gen1-salvage/README.md +0 -45
- package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +0 -116
- package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +0 -293
- package/src/swe-arena/fixtures/holdout-preregister.log +0 -12
- package/src/swe-arena/fixtures/holdout.json +0 -44
- package/src/swe-arena/fixtures/instances.json +0 -146
- package/src/swe-arena/fixtures/ledger.jsonl +0 -12
- package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +0 -36
- package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +0 -33
- package/src/swe-arena/fixtures/rejudge.jsonl +0 -15
- package/src/swe-arena/fixtures/rematch.jsonl +0 -3
- package/src/swe-arena/fixtures/rematch2.jsonl +0 -3
- package/src/swe-arena/fixtures/rematch3.jsonl +0 -3
- package/src/swe-arena/fixtures/run-report/README.md +0 -43
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +0 -173
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +0 -100
- package/src/swe-arena/fixtures/run-report/gen3-rollup.json +0 -551
- package/src/swe-arena/fixtures/run-report/gen3-rollup.md +0 -64
- package/src/swe-arena/fixtures/sup-journal-true.json +0 -19
- package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +0 -48
- package/src/swe-arena/fixtures/verify/django__django-11532.sh +0 -50
- package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +0 -76
- package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +0 -44
- package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +0 -32
- package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +0 -51
- package/src/swe-arena/fixtures/worker-tokens.json +0 -42
- package/src/swe-arena/fixtures.ts +0 -237
- package/src/swe-arena/gepa-seat.mts +0 -886
- package/src/swe-arena/gepa-seat.test.mts +0 -1136
- package/src/swe-arena/holdout-certify.mts +0 -408
- package/src/swe-arena/holdout-certify.test.mts +0 -160
- package/src/swe-arena/implementation-ref.test.mts +0 -64
- package/src/swe-arena/implementation-ref.ts +0 -62
- package/src/swe-arena/judge-child.mts +0 -37
- package/src/swe-arena/ledger-orphans.mts +0 -77
- package/src/swe-arena/ledger-orphans.test.mts +0 -149
- package/src/swe-arena/manifest.mts +0 -293
- package/src/swe-arena/manifest.test.mts +0 -169
- package/src/swe-arena/materialize.ts +0 -142
- package/src/swe-arena/outer-loop.mts +0 -2854
- package/src/swe-arena/outer-loop.test.mts +0 -714
- package/src/swe-arena/parity.test.mts +0 -87
- package/src/swe-arena/premeasured-from-cells.mts +0 -296
- package/src/swe-arena/premeasured-from-cells.test.mts +0 -201
- package/src/swe-arena/proc.test.mts +0 -172
- package/src/swe-arena/proc.ts +0 -260
- package/src/swe-arena/profiles/deepseek-author.profile.json +0 -12
- package/src/swe-arena/profiles/default-author.profile.json +0 -12
- package/src/swe-arena/proposer-fanout.mts +0 -736
- package/src/swe-arena/proposer-fanout.test.mts +0 -660
- package/src/swe-arena/proposer-provenance.mts +0 -176
- package/src/swe-arena/proposer-provenance.test.mts +0 -106
- package/src/swe-arena/reconcile.ts +0 -0
- package/src/swe-arena/replay.mts +0 -183
- package/src/swe-arena/replay.test.mts +0 -300
- package/src/swe-arena/run-experiment.mts +0 -729
- package/src/swe-arena/run-report.mts +0 -75
- package/src/swe-arena/run-supervisor.mjs +0 -297
- package/src/swe-arena/run-supervisor.test.mts +0 -539
- package/src/swe-arena/score-split.mts +0 -140
- package/src/swe-arena/score-split.test.mts +0 -123
- package/src/swe-arena/scratch-worktree-serialization.test.mts +0 -72
- package/src/swe-arena/scratch-worktree.test.mts +0 -56
- package/src/swe-arena/scratch-worktree.ts +0 -64
- package/src/swe-arena/serialized-judge.ts +0 -414
- package/src/swe-arena/types.ts +0 -218
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gen-5 public/private score split: determinism (seeded by runId), persisted
|
|
3
|
-
* resume stability, and the never-surfaced invariant's checkable pieces.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
7
|
-
import { tmpdir } from 'node:os'
|
|
8
|
-
import { join } from 'node:path'
|
|
9
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
10
|
-
import {
|
|
11
|
-
leaksPrivateInstance,
|
|
12
|
-
loadOrCreateScoreSplit,
|
|
13
|
-
publicOnly,
|
|
14
|
-
SCORE_SPLIT_FILENAME,
|
|
15
|
-
splitInstances,
|
|
16
|
-
subScores,
|
|
17
|
-
} from './score-split.mts'
|
|
18
|
-
|
|
19
|
-
const SIX = [
|
|
20
|
-
'astropy__astropy-13033',
|
|
21
|
-
'django__django-11532',
|
|
22
|
-
'matplotlib__matplotlib-20826',
|
|
23
|
-
'pydata__xarray-4687',
|
|
24
|
-
'pytest-dev__pytest-6197',
|
|
25
|
-
'sphinx-doc__sphinx-9658',
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
describe('splitInstances', () => {
|
|
29
|
-
it('is deterministic: same (runId, instances, publicCount) → identical split', () => {
|
|
30
|
-
const a = splitInstances('r4-abc123', SIX, 4)
|
|
31
|
-
const b = splitInstances('r4-abc123', SIX, 4)
|
|
32
|
-
expect(a).toEqual(b)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('is instance-ORDER-insensitive (ranking is content-derived)', () => {
|
|
36
|
-
const a = splitInstances('r4-abc123', SIX, 4)
|
|
37
|
-
const b = splitInstances('r4-abc123', [...SIX].reverse(), 4)
|
|
38
|
-
expect(a).toEqual(b)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('respects publicCount and partitions exactly (4 public / 2 private of 6)', () => {
|
|
42
|
-
const split = splitInstances('r4-abc123', SIX, 4)
|
|
43
|
-
expect(split.publicInstances).toHaveLength(4)
|
|
44
|
-
expect(split.privateInstances).toHaveLength(2)
|
|
45
|
-
expect([...split.publicInstances, ...split.privateInstances].sort()).toEqual([...SIX].sort())
|
|
46
|
-
expect(split.publicInstances.filter((i) => split.privateInstances.includes(i))).toEqual([])
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('is SEEDED by runId: different runIds produce different splits', () => {
|
|
50
|
-
const splits = new Set(
|
|
51
|
-
Array.from({ length: 10 }, (_, i) => splitInstances(`r4-run${i}`, SIX, 4).privateInstances.join('|')),
|
|
52
|
-
)
|
|
53
|
-
expect(splits.size).toBeGreaterThan(1)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('rejects publicCount outside (0, n) and duplicate instance ids', () => {
|
|
57
|
-
expect(() => splitInstances('r', SIX, 0)).toThrow(/publicCount/)
|
|
58
|
-
expect(() => splitInstances('r', SIX, 6)).toThrow(/publicCount/)
|
|
59
|
-
expect(() => splitInstances('r', SIX, 2.5)).toThrow(/publicCount/)
|
|
60
|
-
expect(() => splitInstances('r', ['a', 'a', 'b'], 1)).toThrow(/duplicate/)
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
describe('loadOrCreateScoreSplit (resume stability)', () => {
|
|
65
|
-
let outDir: string
|
|
66
|
-
beforeEach(async () => {
|
|
67
|
-
outDir = await mkdtemp(join(tmpdir(), 'split-'))
|
|
68
|
-
})
|
|
69
|
-
afterEach(async () => {
|
|
70
|
-
await rm(outDir, { recursive: true, force: true })
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('persists on first computation and RELOADS verbatim under a NEW runId (a resume cannot rotate private into view)', async () => {
|
|
74
|
-
const first = await loadOrCreateScoreSplit({ outDir, runId: 'r4-first', instances: SIX, publicCount: 4 })
|
|
75
|
-
expect(JSON.parse(await readFile(join(outDir, SCORE_SPLIT_FILENAME), 'utf8'))).toEqual(first)
|
|
76
|
-
const resumed = await loadOrCreateScoreSplit({ outDir, runId: 'r4-DIFFERENT', instances: SIX, publicCount: 4 })
|
|
77
|
-
expect(resumed).toEqual(first)
|
|
78
|
-
expect(resumed.seededBy).toBe('r4-first')
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('fails loud when the persisted split covers different instances or a different publicCount', async () => {
|
|
82
|
-
await loadOrCreateScoreSplit({ outDir, runId: 'r4-first', instances: SIX, publicCount: 4 })
|
|
83
|
-
await expect(
|
|
84
|
-
loadOrCreateScoreSplit({ outDir, runId: 'r4-x', instances: [...SIX.slice(0, 5), 'other__iid-1'], publicCount: 4 }),
|
|
85
|
-
).rejects.toThrow(/refusing to silently re-split/)
|
|
86
|
-
await expect(
|
|
87
|
-
loadOrCreateScoreSplit({ outDir, runId: 'r4-x', instances: SIX, publicCount: 3 }),
|
|
88
|
-
).rejects.toThrow(/refusing to silently re-split/)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('fails loud on an unknown schema in the persisted file', async () => {
|
|
92
|
-
await writeFile(join(outDir, SCORE_SPLIT_FILENAME), JSON.stringify({ schema: 'nope' }))
|
|
93
|
-
await expect(
|
|
94
|
-
loadOrCreateScoreSplit({ outDir, runId: 'r', instances: SIX, publicCount: 4 }),
|
|
95
|
-
).rejects.toThrow(/unknown schema/)
|
|
96
|
-
})
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
describe('never-surfaced invariant helpers', () => {
|
|
100
|
-
const split = splitInstances('r4-abc123', SIX, 4)
|
|
101
|
-
|
|
102
|
-
it('leaksPrivateInstance names every private id present in a text', () => {
|
|
103
|
-
expect(leaksPrivateInstance('nothing to see', split)).toEqual([])
|
|
104
|
-
const leakText = `evidence for ${split.privateInstances[0]} here`
|
|
105
|
-
expect(leaksPrivateInstance(leakText, split)).toEqual([split.privateInstances[0]])
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('publicOnly drops private-instance records and is identity without a split', () => {
|
|
109
|
-
const records = SIX.map((iid) => ({ iid, x: 1 }))
|
|
110
|
-
const filtered = publicOnly(records, (r) => r.iid, split)
|
|
111
|
-
expect(filtered.map((r) => r.iid).sort()).toEqual(split.publicInstances)
|
|
112
|
-
expect(publicOnly(records, (r) => r.iid, null)).toEqual(records)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('subScores counts fail-closed AND-verdicts per half (missing verdict = 0)', () => {
|
|
116
|
-
const verdicts: Record<string, boolean> = {}
|
|
117
|
-
verdicts[split.publicInstances[0]!] = true
|
|
118
|
-
verdicts[split.publicInstances[1]!] = false
|
|
119
|
-
verdicts[split.privateInstances[0]!] = true
|
|
120
|
-
// privateInstances[1] has NO verdict — counts 0.
|
|
121
|
-
expect(subScores(verdicts, split)).toEqual({ publicResolvedCount: 1, privateResolvedCount: 1 })
|
|
122
|
-
})
|
|
123
|
-
})
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
-
import { tmpdir } from 'node:os'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
-
|
|
6
|
-
const gitCalls = vi.hoisted(() => ({
|
|
7
|
-
activeByRepository: new Map<string, number>(),
|
|
8
|
-
maximumByRepository: new Map<string, number>(),
|
|
9
|
-
active: 0,
|
|
10
|
-
maximum: 0,
|
|
11
|
-
}))
|
|
12
|
-
|
|
13
|
-
vi.mock('./proc', () => {
|
|
14
|
-
const recordGitCall = async (_binary: string, arguments_: string[]) => {
|
|
15
|
-
const repository = arguments_[1] ?? ''
|
|
16
|
-
const activeForRepository = (gitCalls.activeByRepository.get(repository) ?? 0) + 1
|
|
17
|
-
gitCalls.activeByRepository.set(repository, activeForRepository)
|
|
18
|
-
gitCalls.maximumByRepository.set(
|
|
19
|
-
repository,
|
|
20
|
-
Math.max(gitCalls.maximumByRepository.get(repository) ?? 0, activeForRepository),
|
|
21
|
-
)
|
|
22
|
-
gitCalls.active += 1
|
|
23
|
-
gitCalls.maximum = Math.max(gitCalls.maximum, gitCalls.active)
|
|
24
|
-
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
25
|
-
gitCalls.active -= 1
|
|
26
|
-
gitCalls.activeByRepository.set(repository, activeForRepository - 1)
|
|
27
|
-
return { code: 0, stdout: '', stderr: '' }
|
|
28
|
-
}
|
|
29
|
-
return {
|
|
30
|
-
run: vi.fn(recordGitCall),
|
|
31
|
-
runOk: vi.fn(recordGitCall),
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
import {
|
|
36
|
-
createDetachedWorktree,
|
|
37
|
-
pruneDetachedWorktrees,
|
|
38
|
-
removeDetachedWorktree,
|
|
39
|
-
} from './scratch-worktree.ts'
|
|
40
|
-
|
|
41
|
-
describe('scratch worktree mutation serialization', () => {
|
|
42
|
-
const roots: string[] = []
|
|
43
|
-
|
|
44
|
-
afterEach(async () => {
|
|
45
|
-
gitCalls.activeByRepository.clear()
|
|
46
|
-
gitCalls.maximumByRepository.clear()
|
|
47
|
-
gitCalls.active = 0
|
|
48
|
-
gitCalls.maximum = 0
|
|
49
|
-
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('serializes metadata changes per repository without blocking separate repositories', async () => {
|
|
53
|
-
const output = await mkdtemp(join(tmpdir(), 'scratch-worktree-serialization-'))
|
|
54
|
-
roots.push(output)
|
|
55
|
-
const operations = ['repository-a', 'repository-b'].flatMap((repository) =>
|
|
56
|
-
Array.from({ length: 4 }, (_, index) => {
|
|
57
|
-
const destination = join(output, `${repository}-candidate-${index}`)
|
|
58
|
-
return [
|
|
59
|
-
createDetachedWorktree(repository, 'commit', destination),
|
|
60
|
-
removeDetachedWorktree(repository, destination),
|
|
61
|
-
pruneDetachedWorktrees(repository),
|
|
62
|
-
]
|
|
63
|
-
}),
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
await Promise.all(operations.flat())
|
|
67
|
-
|
|
68
|
-
expect(gitCalls.maximumByRepository.get('repository-a')).toBe(1)
|
|
69
|
-
expect(gitCalls.maximumByRepository.get('repository-b')).toBe(1)
|
|
70
|
-
expect(gitCalls.maximum).toBe(2)
|
|
71
|
-
})
|
|
72
|
-
})
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
|
-
import { tmpdir } from 'node:os'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
-
import { runOk } from './proc.ts'
|
|
6
|
-
import {
|
|
7
|
-
createDetachedWorktree,
|
|
8
|
-
pruneDetachedWorktrees,
|
|
9
|
-
removeDetachedWorktree,
|
|
10
|
-
} from './scratch-worktree.ts'
|
|
11
|
-
|
|
12
|
-
describe('scratch worktrees', () => {
|
|
13
|
-
const roots: string[] = []
|
|
14
|
-
|
|
15
|
-
afterEach(async () => {
|
|
16
|
-
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('creates and removes a parallel batch without pruning live metadata', async () => {
|
|
20
|
-
const repository = await mkdtemp(join(tmpdir(), 'scratch-worktree-repo-'))
|
|
21
|
-
const output = await mkdtemp(join(tmpdir(), 'scratch-worktree-out-'))
|
|
22
|
-
roots.push(repository, output)
|
|
23
|
-
await runOk('git', ['init', '-q', '-b', 'main', repository])
|
|
24
|
-
await runOk('git', ['-C', repository, 'config', 'core.hooksPath', '/dev/null'])
|
|
25
|
-
await runOk('git', ['-C', repository, 'config', 'user.email', 'test@example.com'])
|
|
26
|
-
await runOk('git', ['-C', repository, 'config', 'user.name', 'Test'])
|
|
27
|
-
await writeFile(join(repository, 'seed.txt'), 'seed\n')
|
|
28
|
-
await runOk('git', ['-C', repository, 'add', 'seed.txt'])
|
|
29
|
-
await runOk('git', ['-C', repository, 'commit', '-q', '-m', 'seed'])
|
|
30
|
-
const commit = (
|
|
31
|
-
await runOk('git', ['-C', repository, 'rev-parse', 'HEAD'])
|
|
32
|
-
).stdout.trim()
|
|
33
|
-
for (let batch = 0; batch < 5; batch += 1) {
|
|
34
|
-
const worktrees = Array.from(
|
|
35
|
-
{ length: 8 },
|
|
36
|
-
(_, index) => join(output, `batch-${batch}-candidate-${index}`),
|
|
37
|
-
)
|
|
38
|
-
await pruneDetachedWorktrees(repository)
|
|
39
|
-
await Promise.all(
|
|
40
|
-
worktrees.map((worktree) => createDetachedWorktree(repository, commit, worktree)),
|
|
41
|
-
)
|
|
42
|
-
expect(
|
|
43
|
-
await Promise.all(
|
|
44
|
-
worktrees.map((worktree) => readFile(join(worktree, 'seed.txt'), 'utf8')),
|
|
45
|
-
),
|
|
46
|
-
).toEqual(Array.from({ length: 8 }, () => 'seed\n'))
|
|
47
|
-
await Promise.all(
|
|
48
|
-
worktrees.map((worktree) => removeDetachedWorktree(repository, worktree)),
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
const listed = (
|
|
52
|
-
await runOk('git', ['-C', repository, 'worktree', 'list', '--porcelain'])
|
|
53
|
-
).stdout
|
|
54
|
-
expect(listed.match(/^worktree /gmu)).toHaveLength(1)
|
|
55
|
-
}, 60_000)
|
|
56
|
-
})
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { rm } from 'node:fs/promises'
|
|
2
|
-
import { run, runOk } from './proc'
|
|
3
|
-
|
|
4
|
-
// Git worktree subcommands mutate shared metadata under .git/worktrees and are
|
|
5
|
-
// not safe to overlap, even when each command targets a different destination.
|
|
6
|
-
const repositoryMutationQueues = new Map<string, Promise<void>>()
|
|
7
|
-
|
|
8
|
-
async function mutateWorktrees<T>(
|
|
9
|
-
repository: string,
|
|
10
|
-
operation: () => Promise<T>,
|
|
11
|
-
): Promise<T> {
|
|
12
|
-
const predecessor = repositoryMutationQueues.get(repository) ?? Promise.resolve()
|
|
13
|
-
const result = predecessor.then(operation, operation)
|
|
14
|
-
const settled = result.then(
|
|
15
|
-
() => undefined,
|
|
16
|
-
() => undefined,
|
|
17
|
-
)
|
|
18
|
-
repositoryMutationQueues.set(repository, settled)
|
|
19
|
-
try {
|
|
20
|
-
return await result
|
|
21
|
-
} finally {
|
|
22
|
-
if (repositoryMutationQueues.get(repository) === settled) {
|
|
23
|
-
repositoryMutationQueues.delete(repository)
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function createDetachedWorktree(
|
|
29
|
-
repository: string,
|
|
30
|
-
commit: string,
|
|
31
|
-
destination: string,
|
|
32
|
-
): Promise<void> {
|
|
33
|
-
await mutateWorktrees(repository, async () => {
|
|
34
|
-
await run('git', ['-C', repository, 'worktree', 'remove', '--force', '--', destination])
|
|
35
|
-
await rm(destination, { recursive: true, force: true })
|
|
36
|
-
await runOk('git', ['-C', repository, 'worktree', 'add', '--detach', destination, commit])
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function pruneDetachedWorktrees(repository: string): Promise<void> {
|
|
41
|
-
await mutateWorktrees(repository, async () => {
|
|
42
|
-
await runOk('git', ['-C', repository, 'worktree', 'prune'])
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function removeDetachedWorktree(
|
|
47
|
-
repository: string,
|
|
48
|
-
destination: string,
|
|
49
|
-
): Promise<void> {
|
|
50
|
-
await mutateWorktrees(repository, async () => {
|
|
51
|
-
const result = await run('git', [
|
|
52
|
-
'-C',
|
|
53
|
-
repository,
|
|
54
|
-
'worktree',
|
|
55
|
-
'remove',
|
|
56
|
-
'--force',
|
|
57
|
-
'--',
|
|
58
|
-
destination,
|
|
59
|
-
])
|
|
60
|
-
if (result.code !== 0) {
|
|
61
|
-
await rm(destination, { recursive: true, force: true })
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
}
|