solidity-argus 0.2.0 → 0.3.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/AGENTS.md +3 -3
- package/README.md +93 -37
- package/package.json +34 -7
- package/skills/INVENTORY.md +88 -57
- package/skills/README.md +26 -23
- package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
- package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
- package/skills/case-studies/cream-finance/SKILL.md +52 -0
- package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
- package/skills/case-studies/dao-hack/SKILL.md +51 -0
- package/skills/case-studies/euler-finance/SKILL.md +52 -0
- package/skills/case-studies/harvest-finance/SKILL.md +52 -0
- package/skills/case-studies/level-finance/SKILL.md +51 -0
- package/skills/case-studies/mango-markets/SKILL.md +53 -0
- package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
- package/skills/case-studies/parity-multisig/SKILL.md +55 -0
- package/skills/case-studies/poly-network/SKILL.md +51 -0
- package/skills/case-studies/rari-fuse/SKILL.md +51 -0
- package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
- package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
- package/skills/manifests/smartbugs.json +1 -3
- package/skills/manifests/sunweb3sec.json +1 -3
- package/skills/vulnerability-patterns/access-control/SKILL.md +14 -0
- package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
- package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
- package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +2 -1
- package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
- package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +2 -1
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
- package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
- package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +1 -0
- package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
- package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
- package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
- package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
- package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
- package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
- package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
- package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
- package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
- package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
- package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
- package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
- package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
- package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
- package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +9 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
- package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -0
- package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +2 -1
- package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
- package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +2 -1
- package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
- package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
- package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
- package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
- package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
- package/src/agents/argus-prompt.ts +34 -7
- package/src/agents/pythia-prompt.ts +13 -4
- package/src/agents/scribe-prompt.ts +20 -2
- package/src/agents/sentinel-prompt.ts +45 -5
- package/src/cli/cli-program.ts +29 -26
- package/src/cli/commands/check-skills.ts +135 -0
- package/src/cli/commands/doctor.ts +48 -26
- package/src/cli/commands/init.ts +5 -3
- package/src/cli/commands/install.ts +7 -5
- package/src/cli/commands/lint-skills.ts +16 -12
- package/src/cli/index.ts +5 -5
- package/src/cli/types.ts +3 -3
- package/src/config/index.ts +1 -1
- package/src/config/loader.ts +4 -6
- package/src/config/schema.ts +6 -5
- package/src/config/types.ts +2 -2
- package/src/constants/defaults.ts +2 -0
- package/src/create-hooks.ts +145 -34
- package/src/create-managers.ts +10 -8
- package/src/create-tools.ts +13 -9
- package/src/features/background-agent/background-manager.ts +93 -87
- package/src/features/background-agent/index.ts +1 -1
- package/src/features/context-monitor/context-monitor.ts +3 -3
- package/src/features/context-monitor/index.ts +2 -2
- package/src/features/error-recovery/session-recovery.ts +2 -4
- package/src/features/error-recovery/tool-error-recovery.ts +12 -7
- package/src/features/index.ts +5 -5
- package/src/features/persistent-state/audit-state-manager.ts +143 -60
- package/src/features/persistent-state/global-run-index.ts +38 -0
- package/src/features/persistent-state/index.ts +1 -1
- package/src/features/persistent-state/run-journal.ts +86 -0
- package/src/hooks/config-handler.ts +28 -11
- package/src/hooks/context-budget.ts +2 -5
- package/src/hooks/event-hook.ts +47 -23
- package/src/hooks/hook-system.ts +4 -4
- package/src/hooks/index.ts +5 -5
- package/src/hooks/knowledge-sync-hook.ts +18 -21
- package/src/hooks/recon-context-builder.ts +2 -2
- package/src/hooks/safe-create-hook.ts +6 -7
- package/src/hooks/system-prompt-hook.ts +18 -1
- package/src/hooks/tool-tracking-hook.ts +110 -51
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +24 -37
- package/src/knowledge/retry.ts +22 -22
- package/src/knowledge/scvd-client.ts +88 -95
- package/src/knowledge/scvd-errors.ts +35 -35
- package/src/knowledge/scvd-index.ts +78 -80
- package/src/knowledge/scvd-sync.ts +106 -101
- package/src/managers/index.ts +1 -1
- package/src/managers/types.ts +19 -14
- package/src/plugin-interface.ts +7 -9
- package/src/shared/binary-utils.ts +44 -35
- package/src/shared/deep-merge.ts +55 -36
- package/src/shared/file-utils.ts +21 -19
- package/src/shared/index.ts +11 -5
- package/src/shared/jsonc-parser.ts +123 -28
- package/src/shared/logger.ts +16 -3
- package/src/shared/project-utils.ts +30 -0
- package/src/skills/analysis/cluster.ts +414 -0
- package/src/skills/analysis/gates.ts +227 -0
- package/src/skills/analysis/index.ts +33 -0
- package/src/skills/analysis/normalize.ts +217 -0
- package/src/skills/analysis/similarity.ts +224 -0
- package/src/skills/argus-skill-resolver.ts +17 -6
- package/src/skills/skill-schema.ts +11 -10
- package/src/solodit-lifecycle.ts +203 -0
- package/src/state/audit-state.ts +8 -8
- package/src/state/finding-store.ts +68 -55
- package/src/state/types.ts +88 -67
- package/src/tools/argus-skill-load-tool.ts +12 -7
- package/src/tools/contract-analyzer-tool.ts +142 -77
- package/src/tools/forge-coverage-tool.ts +226 -0
- package/src/tools/forge-fuzz-tool.ts +127 -127
- package/src/tools/forge-test-tool.ts +201 -158
- package/src/tools/gas-analysis-tool.ts +264 -0
- package/src/tools/pattern-checker-tool.ts +203 -191
- package/src/tools/pattern-loader.ts +5 -111
- package/src/tools/pattern-schema.ts +3 -0
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +305 -206
- package/src/tools/slither-tool.ts +266 -218
- package/src/tools/solodit-search-tool.ts +235 -119
- package/src/tools/sync-knowledge-tool.ts +7 -11
- package/src/utils/audit-artifact-detector.ts +28 -29
- package/src/utils/dependency-scanner.ts +37 -37
- package/src/utils/project-detector.ts +111 -124
- package/src/utils/solidity-parser.ts +175 -75
- package/skills/patterns/access-control.yaml +0 -31
- package/skills/patterns/erc4626.yaml +0 -29
- package/skills/patterns/flash-loan.yaml +0 -20
- package/skills/patterns/oracle.yaml +0 -30
- package/skills/patterns/proxy.yaml +0 -30
- package/skills/patterns/reentrancy.yaml +0 -30
- package/skills/patterns/signature.yaml +0 -31
- package/src/hooks/event-hook-v2.ts +0 -99
- package/src/state/plugin-state.ts +0 -14
|
@@ -1,152 +1,198 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { resolveProjectDir } from "../shared/project-utils"
|
|
3
|
+
import { extractJson } from "../utils/solidity-parser"
|
|
2
4
|
|
|
3
5
|
type ForgeTestArgs = {
|
|
4
|
-
target?: string
|
|
5
|
-
match_test?: string
|
|
6
|
-
match_contract?: string
|
|
7
|
-
fork_url?: string
|
|
8
|
-
verbosity?: number
|
|
9
|
-
gas_report?: boolean
|
|
10
|
-
coverage?: boolean
|
|
11
|
-
}
|
|
6
|
+
target?: string
|
|
7
|
+
match_test?: string
|
|
8
|
+
match_contract?: string
|
|
9
|
+
fork_url?: string
|
|
10
|
+
verbosity?: number
|
|
11
|
+
gas_report?: boolean
|
|
12
|
+
coverage?: boolean
|
|
13
|
+
}
|
|
12
14
|
|
|
13
15
|
type NormalizedForgeTestArgs = {
|
|
14
|
-
target: string
|
|
15
|
-
match_test?: string
|
|
16
|
-
match_contract?: string
|
|
17
|
-
fork_url?: string
|
|
18
|
-
verbosity: number
|
|
19
|
-
gas_report?: boolean
|
|
20
|
-
coverage: boolean
|
|
21
|
-
}
|
|
16
|
+
target: string
|
|
17
|
+
match_test?: string
|
|
18
|
+
match_contract?: string
|
|
19
|
+
fork_url?: string
|
|
20
|
+
verbosity: number
|
|
21
|
+
gas_report?: boolean
|
|
22
|
+
coverage: boolean
|
|
23
|
+
}
|
|
22
24
|
|
|
23
25
|
type ForgeTestItem = {
|
|
24
|
-
name: string
|
|
25
|
-
contract: string
|
|
26
|
-
status: "pass" | "fail"
|
|
27
|
-
gas: number
|
|
28
|
-
}
|
|
26
|
+
name: string
|
|
27
|
+
contract: string
|
|
28
|
+
status: "pass" | "fail"
|
|
29
|
+
gas: number
|
|
30
|
+
}
|
|
29
31
|
|
|
30
32
|
type ForgeTestSummary = {
|
|
31
|
-
passed: number
|
|
32
|
-
failed: number
|
|
33
|
-
skipped: number
|
|
34
|
-
total: number
|
|
35
|
-
}
|
|
33
|
+
passed: number
|
|
34
|
+
failed: number
|
|
35
|
+
skipped: number
|
|
36
|
+
total: number
|
|
37
|
+
}
|
|
36
38
|
|
|
37
39
|
type ForgeCoverageFile = {
|
|
38
|
-
path: string
|
|
39
|
-
lines: number
|
|
40
|
-
branches: number
|
|
41
|
-
functions: number
|
|
42
|
-
uncoveredFunctions: string[]
|
|
43
|
-
}
|
|
40
|
+
path: string
|
|
41
|
+
lines: number
|
|
42
|
+
branches: number
|
|
43
|
+
functions: number
|
|
44
|
+
uncoveredFunctions: string[]
|
|
45
|
+
}
|
|
44
46
|
|
|
45
47
|
type ForgeTestResult = {
|
|
46
|
-
success: boolean
|
|
47
|
-
summary: ForgeTestSummary
|
|
48
|
-
tests: ForgeTestItem[]
|
|
49
|
-
gasReport?: Record<string, unknown
|
|
50
|
-
coverageReport?: { files: ForgeCoverageFile[] }
|
|
51
|
-
executionTime: number
|
|
52
|
-
error?: string
|
|
53
|
-
}
|
|
48
|
+
success: boolean
|
|
49
|
+
summary: ForgeTestSummary
|
|
50
|
+
tests: ForgeTestItem[]
|
|
51
|
+
gasReport?: Record<string, unknown>
|
|
52
|
+
coverageReport?: { files: ForgeCoverageFile[] }
|
|
53
|
+
executionTime: number
|
|
54
|
+
error?: string
|
|
55
|
+
}
|
|
54
56
|
|
|
55
57
|
export type ForgeCommandResult = {
|
|
56
|
-
stdout: string
|
|
57
|
-
stderr: string
|
|
58
|
-
exitCode: number
|
|
59
|
-
}
|
|
58
|
+
stdout: string
|
|
59
|
+
stderr: string
|
|
60
|
+
exitCode: number
|
|
61
|
+
}
|
|
60
62
|
|
|
61
63
|
type RunForgeCommand = (
|
|
62
64
|
command: string[],
|
|
63
65
|
signal: AbortSignal,
|
|
64
|
-
cwd: string
|
|
65
|
-
) => Promise<ForgeCommandResult
|
|
66
|
+
cwd: string,
|
|
67
|
+
) => Promise<ForgeCommandResult>
|
|
66
68
|
|
|
67
69
|
type ForgeTestPayload = {
|
|
68
|
-
success?: boolean
|
|
70
|
+
success?: boolean
|
|
69
71
|
tests?:
|
|
70
72
|
| Record<string, Record<string, { status?: string; gas?: number }>>
|
|
71
|
-
| Array<{ name?: string; contract?: string; status?: string; gas?: number }
|
|
73
|
+
| Array<{ name?: string; contract?: string; status?: string; gas?: number }>
|
|
72
74
|
summary?: {
|
|
73
|
-
passed?: number
|
|
74
|
-
failed?: number
|
|
75
|
-
skipped?: number
|
|
76
|
-
total?: number
|
|
77
|
-
}
|
|
78
|
-
gas_report?: Record<string, unknown
|
|
79
|
-
gasReport?: Record<string, unknown
|
|
80
|
-
}
|
|
75
|
+
passed?: number
|
|
76
|
+
failed?: number
|
|
77
|
+
skipped?: number
|
|
78
|
+
total?: number
|
|
79
|
+
}
|
|
80
|
+
gas_report?: Record<string, unknown>
|
|
81
|
+
gasReport?: Record<string, unknown>
|
|
82
|
+
}
|
|
81
83
|
|
|
82
84
|
type CoveragePayload = {
|
|
83
|
-
files?: Array<Record<string, unknown
|
|
84
|
-
coverage?: Record<string, Record<string, unknown
|
|
85
|
-
}
|
|
85
|
+
files?: Array<Record<string, unknown>>
|
|
86
|
+
coverage?: Record<string, Record<string, unknown>>
|
|
87
|
+
}
|
|
86
88
|
|
|
87
89
|
function mapStatus(input?: string): "pass" | "fail" | "skip" {
|
|
88
|
-
const normalized = (input ?? "").toLowerCase()
|
|
90
|
+
const normalized = (input ?? "").toLowerCase()
|
|
89
91
|
if (normalized.includes("skip") || normalized.includes("ignore")) {
|
|
90
|
-
return "skip"
|
|
92
|
+
return "skip"
|
|
91
93
|
}
|
|
92
94
|
if (normalized.includes("pass") || normalized.includes("success")) {
|
|
93
|
-
return "pass"
|
|
95
|
+
return "pass"
|
|
94
96
|
}
|
|
95
|
-
return "fail"
|
|
97
|
+
return "fail"
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
function toNumber(input: unknown, fallback = 0): number {
|
|
99
|
-
return typeof input === "number" && Number.isFinite(input) ? input : fallback
|
|
101
|
+
return typeof input === "number" && Number.isFinite(input) ? input : fallback
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
function parseTests(payload: ForgeTestPayload): {
|
|
103
|
-
tests: ForgeTestItem[]
|
|
104
|
-
summary: ForgeTestSummary
|
|
105
|
+
tests: ForgeTestItem[]
|
|
106
|
+
summary: ForgeTestSummary
|
|
105
107
|
} {
|
|
106
|
-
const collected: Array<ForgeTestItem | { skipped: true }> = []
|
|
108
|
+
const collected: Array<ForgeTestItem | { skipped: true }> = []
|
|
109
|
+
|
|
110
|
+
const topLevelEntries = Object.entries(payload as unknown as Record<string, unknown>)
|
|
111
|
+
if (topLevelEntries.some(([key]) => key.includes(":"))) {
|
|
112
|
+
for (const [topLevelKey, suite] of topLevelEntries) {
|
|
113
|
+
if (!suite || typeof suite !== "object") {
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const suiteRecord = suite as Record<string, unknown>
|
|
118
|
+
const testResults = suiteRecord.test_results
|
|
119
|
+
if (!testResults || typeof testResults !== "object") {
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const contract = topLevelKey.split(":").at(1) ?? topLevelKey
|
|
124
|
+
for (const [name, details] of Object.entries(testResults)) {
|
|
125
|
+
if (!details || typeof details !== "object") {
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const detailsRecord = details as Record<string, unknown>
|
|
130
|
+
const statusValue =
|
|
131
|
+
typeof detailsRecord.status === "string" ? detailsRecord.status : undefined
|
|
132
|
+
const status = mapStatus(statusValue)
|
|
133
|
+
if (status === "skip") {
|
|
134
|
+
collected.push({ skipped: true })
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
107
137
|
|
|
108
|
-
|
|
138
|
+
const kind = detailsRecord.kind
|
|
139
|
+
const kindRecord =
|
|
140
|
+
kind && typeof kind === "object" ? (kind as Record<string, unknown>) : undefined
|
|
141
|
+
const unit = kindRecord?.Unit
|
|
142
|
+
const unitRecord =
|
|
143
|
+
unit && typeof unit === "object" ? (unit as Record<string, unknown>) : undefined
|
|
144
|
+
const fuzz = kindRecord?.Fuzz
|
|
145
|
+
const fuzzRecord =
|
|
146
|
+
fuzz && typeof fuzz === "object" ? (fuzz as Record<string, unknown>) : undefined
|
|
147
|
+
|
|
148
|
+
collected.push({
|
|
149
|
+
name,
|
|
150
|
+
contract,
|
|
151
|
+
status,
|
|
152
|
+
gas: toNumber(unitRecord?.gas ?? fuzzRecord?.mean_gas),
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} else if (Array.isArray(payload.tests)) {
|
|
109
157
|
for (const item of payload.tests) {
|
|
110
|
-
const status = mapStatus(item.status)
|
|
158
|
+
const status = mapStatus(item.status)
|
|
111
159
|
if (status === "skip") {
|
|
112
|
-
collected.push({ skipped: true })
|
|
113
|
-
continue
|
|
160
|
+
collected.push({ skipped: true })
|
|
161
|
+
continue
|
|
114
162
|
}
|
|
115
163
|
collected.push({
|
|
116
164
|
name: item.name ?? "unknown-test",
|
|
117
165
|
contract: item.contract ?? "unknown-contract",
|
|
118
166
|
status,
|
|
119
167
|
gas: toNumber(item.gas),
|
|
120
|
-
})
|
|
168
|
+
})
|
|
121
169
|
}
|
|
122
170
|
} else if (payload.tests && typeof payload.tests === "object") {
|
|
123
|
-
const entries = Object.entries(payload.tests)
|
|
171
|
+
const entries = Object.entries(payload.tests)
|
|
124
172
|
for (const [contract, tests] of entries) {
|
|
125
173
|
for (const [name, details] of Object.entries(tests)) {
|
|
126
|
-
const status = mapStatus(details.status)
|
|
174
|
+
const status = mapStatus(details.status)
|
|
127
175
|
if (status === "skip") {
|
|
128
|
-
collected.push({ skipped: true })
|
|
129
|
-
continue
|
|
176
|
+
collected.push({ skipped: true })
|
|
177
|
+
continue
|
|
130
178
|
}
|
|
131
179
|
collected.push({
|
|
132
180
|
name,
|
|
133
181
|
contract,
|
|
134
182
|
status,
|
|
135
183
|
gas: toNumber(details.gas),
|
|
136
|
-
})
|
|
184
|
+
})
|
|
137
185
|
}
|
|
138
186
|
}
|
|
139
187
|
}
|
|
140
188
|
|
|
141
|
-
const tests = collected.filter((item): item is ForgeTestItem => !("skipped" in item))
|
|
142
|
-
const passed = tests.filter((item) => item.status === "pass").length
|
|
143
|
-
const failed = tests.filter((item) => item.status === "fail").length
|
|
144
|
-
const skippedFromTests = collected.length - tests.length
|
|
145
|
-
const summary = payload.summary
|
|
146
|
-
const skipped =
|
|
147
|
-
|
|
148
|
-
const total =
|
|
149
|
-
typeof summary?.total === "number" ? summary.total : passed + failed + skipped;
|
|
189
|
+
const tests = collected.filter((item): item is ForgeTestItem => !("skipped" in item))
|
|
190
|
+
const passed = tests.filter((item) => item.status === "pass").length
|
|
191
|
+
const failed = tests.filter((item) => item.status === "fail").length
|
|
192
|
+
const skippedFromTests = collected.length - tests.length
|
|
193
|
+
const summary = payload.summary
|
|
194
|
+
const skipped = typeof summary?.skipped === "number" ? summary.skipped : skippedFromTests
|
|
195
|
+
const total = typeof summary?.total === "number" ? summary.total : passed + failed + skipped
|
|
150
196
|
|
|
151
197
|
return {
|
|
152
198
|
tests,
|
|
@@ -156,51 +202,49 @@ function parseTests(payload: ForgeTestPayload): {
|
|
|
156
202
|
skipped,
|
|
157
203
|
total,
|
|
158
204
|
},
|
|
159
|
-
}
|
|
205
|
+
}
|
|
160
206
|
}
|
|
161
207
|
|
|
162
208
|
function valueFromRecord(record: Record<string, unknown>, keys: string[]): unknown {
|
|
163
209
|
for (const key of keys) {
|
|
164
210
|
if (key in record) {
|
|
165
|
-
return record[key]
|
|
211
|
+
return record[key]
|
|
166
212
|
}
|
|
167
213
|
}
|
|
168
|
-
return undefined
|
|
214
|
+
return undefined
|
|
169
215
|
}
|
|
170
216
|
|
|
171
217
|
function parseUncoveredFunctions(input: unknown): string[] {
|
|
172
218
|
if (!Array.isArray(input)) {
|
|
173
|
-
return []
|
|
219
|
+
return []
|
|
174
220
|
}
|
|
175
221
|
|
|
176
222
|
return input
|
|
177
223
|
.map((value) => {
|
|
178
224
|
if (typeof value === "string") {
|
|
179
|
-
return value
|
|
225
|
+
return value
|
|
180
226
|
}
|
|
181
227
|
if (value && typeof value === "object" && "name" in value) {
|
|
182
|
-
const name = (value as { name?: unknown }).name
|
|
183
|
-
return typeof name === "string" ? name : ""
|
|
228
|
+
const name = (value as { name?: unknown }).name
|
|
229
|
+
return typeof name === "string" ? name : ""
|
|
184
230
|
}
|
|
185
|
-
return ""
|
|
231
|
+
return ""
|
|
186
232
|
})
|
|
187
|
-
.filter((value) => value.length > 0)
|
|
233
|
+
.filter((value) => value.length > 0)
|
|
188
234
|
}
|
|
189
235
|
|
|
190
236
|
function normalizeCoverageFile(file: Record<string, unknown>): ForgeCoverageFile {
|
|
191
237
|
return {
|
|
192
238
|
path: (valueFromRecord(file, ["path", "file", "name"]) as string) ?? "unknown",
|
|
193
239
|
lines: toNumber(valueFromRecord(file, ["lines", "lineCoverage", "line_coverage"])),
|
|
194
|
-
branches: toNumber(
|
|
195
|
-
valueFromRecord(file, ["branches", "branchCoverage", "branch_coverage"])
|
|
196
|
-
),
|
|
240
|
+
branches: toNumber(valueFromRecord(file, ["branches", "branchCoverage", "branch_coverage"])),
|
|
197
241
|
functions: toNumber(
|
|
198
|
-
valueFromRecord(file, ["functions", "functionCoverage", "function_coverage"])
|
|
242
|
+
valueFromRecord(file, ["functions", "functionCoverage", "function_coverage"]),
|
|
199
243
|
),
|
|
200
244
|
uncoveredFunctions: parseUncoveredFunctions(
|
|
201
|
-
valueFromRecord(file, ["uncoveredFunctions", "uncovered_functions"])
|
|
245
|
+
valueFromRecord(file, ["uncoveredFunctions", "uncovered_functions"]),
|
|
202
246
|
),
|
|
203
|
-
}
|
|
247
|
+
}
|
|
204
248
|
}
|
|
205
249
|
|
|
206
250
|
function parseCoverage(payload: CoveragePayload): { files: ForgeCoverageFile[] } {
|
|
@@ -209,32 +253,33 @@ function parseCoverage(payload: CoveragePayload): { files: ForgeCoverageFile[] }
|
|
|
209
253
|
files: payload.files
|
|
210
254
|
.filter((item): item is Record<string, unknown> => !!item && typeof item === "object")
|
|
211
255
|
.map((item) => normalizeCoverageFile(item)),
|
|
212
|
-
}
|
|
256
|
+
}
|
|
213
257
|
}
|
|
214
258
|
|
|
215
259
|
if (payload.coverage && typeof payload.coverage === "object") {
|
|
216
|
-
const files: ForgeCoverageFile[] = []
|
|
260
|
+
const files: ForgeCoverageFile[] = []
|
|
217
261
|
for (const [path, metrics] of Object.entries(payload.coverage)) {
|
|
218
262
|
if (!metrics || typeof metrics !== "object") {
|
|
219
|
-
continue
|
|
263
|
+
continue
|
|
220
264
|
}
|
|
221
265
|
files.push(
|
|
222
266
|
normalizeCoverageFile({
|
|
223
267
|
path,
|
|
224
268
|
...metrics,
|
|
225
|
-
})
|
|
226
|
-
)
|
|
269
|
+
}),
|
|
270
|
+
)
|
|
227
271
|
}
|
|
228
272
|
|
|
229
|
-
return { files }
|
|
273
|
+
return { files }
|
|
230
274
|
}
|
|
231
275
|
|
|
232
|
-
return { files: [] }
|
|
276
|
+
return { files: [] }
|
|
233
277
|
}
|
|
234
278
|
|
|
235
|
-
function normalizeArgs(args: ForgeTestArgs): NormalizedForgeTestArgs {
|
|
279
|
+
function normalizeArgs(args: ForgeTestArgs, context: ToolContext): NormalizedForgeTestArgs {
|
|
280
|
+
const target = args.target && args.target !== "." ? args.target : resolveProjectDir(context)
|
|
236
281
|
return {
|
|
237
|
-
target
|
|
282
|
+
target,
|
|
238
283
|
match_test: args.match_test,
|
|
239
284
|
match_contract: args.match_contract,
|
|
240
285
|
fork_url: args.fork_url,
|
|
@@ -244,26 +289,26 @@ function normalizeArgs(args: ForgeTestArgs): NormalizedForgeTestArgs {
|
|
|
244
289
|
: 3,
|
|
245
290
|
gas_report: args.gas_report,
|
|
246
291
|
coverage: args.coverage ?? false,
|
|
247
|
-
}
|
|
292
|
+
}
|
|
248
293
|
}
|
|
249
294
|
|
|
250
295
|
function buildForgeTestCommand(args: NormalizedForgeTestArgs): string[] {
|
|
251
|
-
const command = ["forge", "test", "--json", `-v${"v".repeat(args.verbosity - 1)}`]
|
|
296
|
+
const command = ["forge", "test", "--json", `-v${"v".repeat(args.verbosity - 1)}`]
|
|
252
297
|
|
|
253
298
|
if (args.match_test) {
|
|
254
|
-
command.push("--match-test", args.match_test)
|
|
299
|
+
command.push("--match-test", args.match_test)
|
|
255
300
|
}
|
|
256
301
|
if (args.match_contract) {
|
|
257
|
-
command.push("--match-contract", args.match_contract)
|
|
302
|
+
command.push("--match-contract", args.match_contract)
|
|
258
303
|
}
|
|
259
304
|
if (args.fork_url) {
|
|
260
|
-
command.push("--fork-url", args.fork_url)
|
|
305
|
+
command.push("--fork-url", args.fork_url)
|
|
261
306
|
}
|
|
262
307
|
if (args.gas_report) {
|
|
263
|
-
command.push("--gas-report")
|
|
308
|
+
command.push("--gas-report")
|
|
264
309
|
}
|
|
265
310
|
|
|
266
|
-
return command
|
|
311
|
+
return command
|
|
267
312
|
}
|
|
268
313
|
|
|
269
314
|
const runForgeCommand: RunForgeCommand = async (command, signal, cwd) => {
|
|
@@ -272,29 +317,29 @@ const runForgeCommand: RunForgeCommand = async (command, signal, cwd) => {
|
|
|
272
317
|
stdout: "pipe",
|
|
273
318
|
stderr: "pipe",
|
|
274
319
|
signal,
|
|
275
|
-
})
|
|
320
|
+
})
|
|
276
321
|
|
|
277
322
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
278
323
|
child.exited,
|
|
279
324
|
new Response(child.stdout).text(),
|
|
280
325
|
new Response(child.stderr).text(),
|
|
281
|
-
])
|
|
326
|
+
])
|
|
282
327
|
|
|
283
328
|
return {
|
|
284
329
|
stdout,
|
|
285
330
|
stderr,
|
|
286
331
|
exitCode,
|
|
287
|
-
}
|
|
288
|
-
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
289
334
|
|
|
290
335
|
export async function executeForgeTest(
|
|
291
336
|
args: ForgeTestArgs,
|
|
292
337
|
context: ToolContext,
|
|
293
|
-
runCommand: RunForgeCommand = runForgeCommand
|
|
338
|
+
runCommand: RunForgeCommand = runForgeCommand,
|
|
294
339
|
): Promise<ForgeTestResult> {
|
|
295
|
-
const startedAt = Date.now()
|
|
296
|
-
const normalizedArgs = normalizeArgs(args)
|
|
297
|
-
context.metadata({ title: `Run forge test: ${normalizedArgs.target}` })
|
|
340
|
+
const startedAt = Date.now()
|
|
341
|
+
const normalizedArgs = normalizeArgs(args, context)
|
|
342
|
+
context.metadata({ title: `Run forge test: ${normalizedArgs.target}` })
|
|
298
343
|
|
|
299
344
|
const fail = (error: string): ForgeTestResult => ({
|
|
300
345
|
success: false,
|
|
@@ -302,23 +347,23 @@ export async function executeForgeTest(
|
|
|
302
347
|
tests: [],
|
|
303
348
|
executionTime: Date.now() - startedAt,
|
|
304
349
|
error,
|
|
305
|
-
})
|
|
350
|
+
})
|
|
306
351
|
|
|
307
352
|
try {
|
|
308
353
|
const testResult = await runCommand(
|
|
309
354
|
buildForgeTestCommand(normalizedArgs),
|
|
310
355
|
context.abort,
|
|
311
|
-
normalizedArgs.target
|
|
312
|
-
)
|
|
356
|
+
normalizedArgs.target,
|
|
357
|
+
)
|
|
313
358
|
|
|
314
|
-
let payload: ForgeTestPayload
|
|
359
|
+
let payload: ForgeTestPayload
|
|
315
360
|
try {
|
|
316
|
-
payload = JSON.parse(testResult.stdout) as ForgeTestPayload
|
|
361
|
+
payload = JSON.parse(extractJson(testResult.stdout, "{")) as ForgeTestPayload
|
|
317
362
|
} catch {
|
|
318
|
-
return fail("Invalid JSON output from forge test")
|
|
363
|
+
return fail("Invalid JSON output from forge test")
|
|
319
364
|
}
|
|
320
365
|
|
|
321
|
-
const parsed = parseTests(payload)
|
|
366
|
+
const parsed = parseTests(payload)
|
|
322
367
|
const output: ForgeTestResult = {
|
|
323
368
|
success:
|
|
324
369
|
testResult.exitCode === 0 &&
|
|
@@ -327,55 +372,53 @@ export async function executeForgeTest(
|
|
|
327
372
|
summary: parsed.summary,
|
|
328
373
|
tests: parsed.tests,
|
|
329
374
|
executionTime: Date.now() - startedAt,
|
|
330
|
-
}
|
|
375
|
+
}
|
|
331
376
|
|
|
332
|
-
const gasReport = payload.gas_report ?? payload.gasReport
|
|
377
|
+
const gasReport = payload.gas_report ?? payload.gasReport
|
|
333
378
|
if (gasReport) {
|
|
334
|
-
output.gasReport = gasReport
|
|
379
|
+
output.gasReport = gasReport
|
|
335
380
|
}
|
|
336
381
|
|
|
337
382
|
if (normalizedArgs.coverage) {
|
|
338
383
|
const coverageResult = await runCommand(
|
|
339
384
|
["forge", "coverage", "--report", "json"],
|
|
340
385
|
context.abort,
|
|
341
|
-
normalizedArgs.target
|
|
342
|
-
)
|
|
386
|
+
normalizedArgs.target,
|
|
387
|
+
)
|
|
343
388
|
if (coverageResult.exitCode !== 0) {
|
|
344
|
-
output.error = coverageResult.stderr.trim() || "forge coverage failed"
|
|
345
|
-
output.success = false
|
|
389
|
+
output.error = coverageResult.stderr.trim() || "forge coverage failed"
|
|
390
|
+
output.success = false
|
|
346
391
|
} else {
|
|
347
392
|
try {
|
|
348
|
-
const coveragePayload = JSON.parse(coverageResult.stdout) as CoveragePayload
|
|
349
|
-
output.coverageReport = parseCoverage(coveragePayload)
|
|
393
|
+
const coveragePayload = JSON.parse(coverageResult.stdout) as CoveragePayload
|
|
394
|
+
output.coverageReport = parseCoverage(coveragePayload)
|
|
350
395
|
} catch {
|
|
351
|
-
output.error = "Invalid JSON output from forge coverage"
|
|
352
|
-
output.success = false
|
|
396
|
+
output.error = "Invalid JSON output from forge coverage"
|
|
397
|
+
output.success = false
|
|
353
398
|
}
|
|
354
399
|
}
|
|
355
400
|
}
|
|
356
401
|
|
|
357
402
|
if (testResult.exitCode !== 0 && !output.error) {
|
|
358
|
-
output.error =
|
|
403
|
+
output.error =
|
|
404
|
+
testResult.stderr.trim() || `forge test exited with code ${testResult.exitCode}`
|
|
359
405
|
}
|
|
360
406
|
|
|
361
|
-
return output
|
|
407
|
+
return output
|
|
362
408
|
} catch (error) {
|
|
363
409
|
if (context.abort.aborted || (error instanceof DOMException && error.name === "AbortError")) {
|
|
364
|
-
return fail("forge test aborted")
|
|
410
|
+
return fail("forge test aborted")
|
|
365
411
|
}
|
|
366
412
|
|
|
367
|
-
const maybeError = error as Error & { code?: string }
|
|
413
|
+
const maybeError = error as Error & { code?: string }
|
|
368
414
|
if (maybeError.code === "ENOENT") {
|
|
369
|
-
return fail("Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash")
|
|
415
|
+
return fail("Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash")
|
|
370
416
|
}
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
maybeError.message.toLowerCase().includes("timed out")
|
|
374
|
-
) {
|
|
375
|
-
return fail("forge test timed out");
|
|
417
|
+
if (maybeError.code === "ETIMEDOUT" || maybeError.message.toLowerCase().includes("timed out")) {
|
|
418
|
+
return fail("forge test timed out")
|
|
376
419
|
}
|
|
377
420
|
|
|
378
|
-
return fail(maybeError.message || "forge test failed")
|
|
421
|
+
return fail(maybeError.message || "forge test failed")
|
|
379
422
|
}
|
|
380
423
|
}
|
|
381
424
|
|
|
@@ -391,7 +434,7 @@ export const forgeTestTool = tool({
|
|
|
391
434
|
coverage: tool.schema.boolean().default(false),
|
|
392
435
|
},
|
|
393
436
|
async execute(args, context) {
|
|
394
|
-
const result = await executeForgeTest(args, context)
|
|
395
|
-
return JSON.stringify(result)
|
|
437
|
+
const result = await executeForgeTest(args, context)
|
|
438
|
+
return JSON.stringify(result)
|
|
396
439
|
},
|
|
397
|
-
})
|
|
440
|
+
})
|