solidity-argus 0.1.8 → 0.3.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/AGENTS.md +3 -3
- package/README.md +229 -13
- package/package.json +37 -8
- package/skills/INVENTORY.md +88 -57
- package/skills/README.md +72 -6
- 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/checklists/cyfrin-defi-core/SKILL.md +3 -0
- package/skills/manifests/cyfrin.json +16 -0
- package/skills/manifests/defifofum.json +25 -0
- package/skills/manifests/kadenzipfel.json +48 -0
- package/skills/manifests/scvd.json +9 -0
- package/skills/manifests/smartbugs.json +9 -0
- package/skills/manifests/solodit.json +9 -0
- package/skills/manifests/sunweb3sec.json +9 -0
- package/skills/manifests/trailofbits.json +9 -0
- package/skills/methodology/audit-workflow/SKILL.md +3 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
- package/skills/references/exploit-reference/SKILL.md +3 -0
- package/skills/vulnerability-patterns/access-control/SKILL.md +27 -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 +8 -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 +8 -1
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +14 -1
- 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 +13 -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 +22 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +11 -1
- package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +22 -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 +11 -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 +13 -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 +27 -10
- package/src/agents/pythia-prompt.ts +7 -8
- package/src/agents/scribe-prompt.ts +10 -5
- package/src/agents/sentinel-prompt.ts +36 -7
- package/src/cli/cli-output.ts +16 -0
- package/src/cli/cli-program.ts +29 -22
- package/src/cli/commands/check-skills.ts +135 -0
- package/src/cli/commands/doctor.ts +303 -23
- package/src/cli/commands/init.ts +8 -6
- package/src/cli/commands/install.ts +10 -8
- package/src/cli/commands/lint-skills.ts +118 -0
- package/src/cli/index.ts +5 -5
- package/src/cli/tui-prompts.ts +4 -2
- 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 +225 -29
- package/src/create-managers.ts +10 -8
- package/src/create-tools.ts +14 -8
- 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 +79 -19
- package/src/features/index.ts +5 -5
- package/src/features/persistent-state/audit-state-manager.ts +158 -52
- 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/agent-tracker.ts +53 -0
- package/src/hooks/compaction-hook.ts +46 -37
- package/src/hooks/config-handler.ts +31 -11
- package/src/hooks/context-budget.ts +42 -0
- package/src/hooks/event-hook.ts +48 -23
- package/src/hooks/hook-system.ts +4 -4
- package/src/hooks/index.ts +5 -5
- package/src/hooks/knowledge-sync-hook.ts +19 -21
- package/src/hooks/recon-context-builder.ts +66 -0
- package/src/hooks/safe-create-hook.ts +9 -11
- package/src/hooks/system-prompt-hook.ts +128 -0
- package/src/hooks/tool-tracking-hook.ts +162 -29
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +23 -13
- package/src/knowledge/retry.ts +53 -0
- package/src/knowledge/scvd-client.ts +103 -83
- package/src/knowledge/scvd-errors.ts +89 -0
- package/src/knowledge/scvd-index.ts +110 -62
- package/src/knowledge/scvd-sync.ts +223 -47
- package/src/knowledge/source-manifest.ts +102 -0
- package/src/managers/index.ts +1 -1
- package/src/managers/types.ts +19 -14
- package/src/plugin-interface.ts +19 -8
- package/src/shared/binary-utils.ts +44 -34
- 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 +91 -17
- 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 +237 -0
- package/src/skills/skill-schema.ts +99 -0
- package/src/solodit-lifecycle.ts +202 -0
- package/src/state/audit-state.ts +10 -8
- package/src/state/finding-store.ts +68 -55
- package/src/state/types.ts +96 -44
- package/src/tools/argus-skill-load-tool.ts +78 -0
- package/src/tools/contract-analyzer-tool.ts +60 -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 +153 -157
- package/src/tools/gas-analysis-tool.ts +264 -0
- package/src/tools/pattern-checker-tool.ts +206 -167
- package/src/tools/pattern-loader.ts +77 -0
- package/src/tools/pattern-schema.ts +51 -0
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +333 -142
- package/src/tools/slither-tool.ts +300 -210
- package/src/tools/solodit-search-tool.ts +255 -80
- package/src/tools/sync-knowledge-tool.ts +7 -11
- package/src/utils/audit-artifact-detector.ts +118 -0
- package/src/utils/dependency-scanner.ts +93 -0
- package/src/utils/project-detector.ts +175 -86
- package/src/utils/solidity-parser.ts +112 -67
- package/src/utils/solodit-health.ts +29 -0
- package/src/hooks/event-hook-v2.ts +0 -99
- package/src/state/plugin-state.ts +0 -14
|
@@ -1,152 +1,151 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { resolveProjectDir } from "../shared/project-utils"
|
|
2
3
|
|
|
3
4
|
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
|
-
}
|
|
5
|
+
target?: string
|
|
6
|
+
match_test?: string
|
|
7
|
+
match_contract?: string
|
|
8
|
+
fork_url?: string
|
|
9
|
+
verbosity?: number
|
|
10
|
+
gas_report?: boolean
|
|
11
|
+
coverage?: boolean
|
|
12
|
+
}
|
|
12
13
|
|
|
13
14
|
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
|
-
}
|
|
15
|
+
target: string
|
|
16
|
+
match_test?: string
|
|
17
|
+
match_contract?: string
|
|
18
|
+
fork_url?: string
|
|
19
|
+
verbosity: number
|
|
20
|
+
gas_report?: boolean
|
|
21
|
+
coverage: boolean
|
|
22
|
+
}
|
|
22
23
|
|
|
23
24
|
type ForgeTestItem = {
|
|
24
|
-
name: string
|
|
25
|
-
contract: string
|
|
26
|
-
status: "pass" | "fail"
|
|
27
|
-
gas: number
|
|
28
|
-
}
|
|
25
|
+
name: string
|
|
26
|
+
contract: string
|
|
27
|
+
status: "pass" | "fail"
|
|
28
|
+
gas: number
|
|
29
|
+
}
|
|
29
30
|
|
|
30
31
|
type ForgeTestSummary = {
|
|
31
|
-
passed: number
|
|
32
|
-
failed: number
|
|
33
|
-
skipped: number
|
|
34
|
-
total: number
|
|
35
|
-
}
|
|
32
|
+
passed: number
|
|
33
|
+
failed: number
|
|
34
|
+
skipped: number
|
|
35
|
+
total: number
|
|
36
|
+
}
|
|
36
37
|
|
|
37
38
|
type ForgeCoverageFile = {
|
|
38
|
-
path: string
|
|
39
|
-
lines: number
|
|
40
|
-
branches: number
|
|
41
|
-
functions: number
|
|
42
|
-
uncoveredFunctions: string[]
|
|
43
|
-
}
|
|
39
|
+
path: string
|
|
40
|
+
lines: number
|
|
41
|
+
branches: number
|
|
42
|
+
functions: number
|
|
43
|
+
uncoveredFunctions: string[]
|
|
44
|
+
}
|
|
44
45
|
|
|
45
46
|
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
|
-
}
|
|
47
|
+
success: boolean
|
|
48
|
+
summary: ForgeTestSummary
|
|
49
|
+
tests: ForgeTestItem[]
|
|
50
|
+
gasReport?: Record<string, unknown>
|
|
51
|
+
coverageReport?: { files: ForgeCoverageFile[] }
|
|
52
|
+
executionTime: number
|
|
53
|
+
error?: string
|
|
54
|
+
}
|
|
54
55
|
|
|
55
56
|
export type ForgeCommandResult = {
|
|
56
|
-
stdout: string
|
|
57
|
-
stderr: string
|
|
58
|
-
exitCode: number
|
|
59
|
-
}
|
|
57
|
+
stdout: string
|
|
58
|
+
stderr: string
|
|
59
|
+
exitCode: number
|
|
60
|
+
}
|
|
60
61
|
|
|
61
62
|
type RunForgeCommand = (
|
|
62
63
|
command: string[],
|
|
63
64
|
signal: AbortSignal,
|
|
64
|
-
cwd: string
|
|
65
|
-
) => Promise<ForgeCommandResult
|
|
65
|
+
cwd: string,
|
|
66
|
+
) => Promise<ForgeCommandResult>
|
|
66
67
|
|
|
67
68
|
type ForgeTestPayload = {
|
|
68
|
-
success?: boolean
|
|
69
|
+
success?: boolean
|
|
69
70
|
tests?:
|
|
70
71
|
| Record<string, Record<string, { status?: string; gas?: number }>>
|
|
71
|
-
| Array<{ name?: string; contract?: string; status?: string; gas?: number }
|
|
72
|
+
| Array<{ name?: string; contract?: string; status?: string; gas?: number }>
|
|
72
73
|
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
|
-
}
|
|
74
|
+
passed?: number
|
|
75
|
+
failed?: number
|
|
76
|
+
skipped?: number
|
|
77
|
+
total?: number
|
|
78
|
+
}
|
|
79
|
+
gas_report?: Record<string, unknown>
|
|
80
|
+
gasReport?: Record<string, unknown>
|
|
81
|
+
}
|
|
81
82
|
|
|
82
83
|
type CoveragePayload = {
|
|
83
|
-
files?: Array<Record<string, unknown
|
|
84
|
-
coverage?: Record<string, Record<string, unknown
|
|
85
|
-
}
|
|
84
|
+
files?: Array<Record<string, unknown>>
|
|
85
|
+
coverage?: Record<string, Record<string, unknown>>
|
|
86
|
+
}
|
|
86
87
|
|
|
87
88
|
function mapStatus(input?: string): "pass" | "fail" | "skip" {
|
|
88
|
-
const normalized = (input ?? "").toLowerCase()
|
|
89
|
+
const normalized = (input ?? "").toLowerCase()
|
|
89
90
|
if (normalized.includes("skip") || normalized.includes("ignore")) {
|
|
90
|
-
return "skip"
|
|
91
|
+
return "skip"
|
|
91
92
|
}
|
|
92
93
|
if (normalized.includes("pass") || normalized.includes("success")) {
|
|
93
|
-
return "pass"
|
|
94
|
+
return "pass"
|
|
94
95
|
}
|
|
95
|
-
return "fail"
|
|
96
|
+
return "fail"
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
function toNumber(input: unknown, fallback = 0): number {
|
|
99
|
-
return typeof input === "number" && Number.isFinite(input) ? input : fallback
|
|
100
|
+
return typeof input === "number" && Number.isFinite(input) ? input : fallback
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
function parseTests(payload: ForgeTestPayload): {
|
|
103
|
-
tests: ForgeTestItem[]
|
|
104
|
-
summary: ForgeTestSummary
|
|
104
|
+
tests: ForgeTestItem[]
|
|
105
|
+
summary: ForgeTestSummary
|
|
105
106
|
} {
|
|
106
|
-
const collected: Array<ForgeTestItem | { skipped: true }> = []
|
|
107
|
+
const collected: Array<ForgeTestItem | { skipped: true }> = []
|
|
107
108
|
|
|
108
109
|
if (Array.isArray(payload.tests)) {
|
|
109
110
|
for (const item of payload.tests) {
|
|
110
|
-
const status = mapStatus(item.status)
|
|
111
|
+
const status = mapStatus(item.status)
|
|
111
112
|
if (status === "skip") {
|
|
112
|
-
collected.push({ skipped: true })
|
|
113
|
-
continue
|
|
113
|
+
collected.push({ skipped: true })
|
|
114
|
+
continue
|
|
114
115
|
}
|
|
115
116
|
collected.push({
|
|
116
117
|
name: item.name ?? "unknown-test",
|
|
117
118
|
contract: item.contract ?? "unknown-contract",
|
|
118
119
|
status,
|
|
119
120
|
gas: toNumber(item.gas),
|
|
120
|
-
})
|
|
121
|
+
})
|
|
121
122
|
}
|
|
122
123
|
} else if (payload.tests && typeof payload.tests === "object") {
|
|
123
|
-
const entries = Object.entries(payload.tests)
|
|
124
|
+
const entries = Object.entries(payload.tests)
|
|
124
125
|
for (const [contract, tests] of entries) {
|
|
125
126
|
for (const [name, details] of Object.entries(tests)) {
|
|
126
|
-
const status = mapStatus(details.status)
|
|
127
|
+
const status = mapStatus(details.status)
|
|
127
128
|
if (status === "skip") {
|
|
128
|
-
collected.push({ skipped: true })
|
|
129
|
-
continue
|
|
129
|
+
collected.push({ skipped: true })
|
|
130
|
+
continue
|
|
130
131
|
}
|
|
131
132
|
collected.push({
|
|
132
133
|
name,
|
|
133
134
|
contract,
|
|
134
135
|
status,
|
|
135
136
|
gas: toNumber(details.gas),
|
|
136
|
-
})
|
|
137
|
+
})
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
|
|
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;
|
|
142
|
+
const tests = collected.filter((item): item is ForgeTestItem => !("skipped" in item))
|
|
143
|
+
const passed = tests.filter((item) => item.status === "pass").length
|
|
144
|
+
const failed = tests.filter((item) => item.status === "fail").length
|
|
145
|
+
const skippedFromTests = collected.length - tests.length
|
|
146
|
+
const summary = payload.summary
|
|
147
|
+
const skipped = typeof summary?.skipped === "number" ? summary.skipped : skippedFromTests
|
|
148
|
+
const total = typeof summary?.total === "number" ? summary.total : passed + failed + skipped
|
|
150
149
|
|
|
151
150
|
return {
|
|
152
151
|
tests,
|
|
@@ -156,51 +155,49 @@ function parseTests(payload: ForgeTestPayload): {
|
|
|
156
155
|
skipped,
|
|
157
156
|
total,
|
|
158
157
|
},
|
|
159
|
-
}
|
|
158
|
+
}
|
|
160
159
|
}
|
|
161
160
|
|
|
162
161
|
function valueFromRecord(record: Record<string, unknown>, keys: string[]): unknown {
|
|
163
162
|
for (const key of keys) {
|
|
164
163
|
if (key in record) {
|
|
165
|
-
return record[key]
|
|
164
|
+
return record[key]
|
|
166
165
|
}
|
|
167
166
|
}
|
|
168
|
-
return undefined
|
|
167
|
+
return undefined
|
|
169
168
|
}
|
|
170
169
|
|
|
171
170
|
function parseUncoveredFunctions(input: unknown): string[] {
|
|
172
171
|
if (!Array.isArray(input)) {
|
|
173
|
-
return []
|
|
172
|
+
return []
|
|
174
173
|
}
|
|
175
174
|
|
|
176
175
|
return input
|
|
177
176
|
.map((value) => {
|
|
178
177
|
if (typeof value === "string") {
|
|
179
|
-
return value
|
|
178
|
+
return value
|
|
180
179
|
}
|
|
181
180
|
if (value && typeof value === "object" && "name" in value) {
|
|
182
|
-
const name = (value as { name?: unknown }).name
|
|
183
|
-
return typeof name === "string" ? name : ""
|
|
181
|
+
const name = (value as { name?: unknown }).name
|
|
182
|
+
return typeof name === "string" ? name : ""
|
|
184
183
|
}
|
|
185
|
-
return ""
|
|
184
|
+
return ""
|
|
186
185
|
})
|
|
187
|
-
.filter((value) => value.length > 0)
|
|
186
|
+
.filter((value) => value.length > 0)
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
function normalizeCoverageFile(file: Record<string, unknown>): ForgeCoverageFile {
|
|
191
190
|
return {
|
|
192
191
|
path: (valueFromRecord(file, ["path", "file", "name"]) as string) ?? "unknown",
|
|
193
192
|
lines: toNumber(valueFromRecord(file, ["lines", "lineCoverage", "line_coverage"])),
|
|
194
|
-
branches: toNumber(
|
|
195
|
-
valueFromRecord(file, ["branches", "branchCoverage", "branch_coverage"])
|
|
196
|
-
),
|
|
193
|
+
branches: toNumber(valueFromRecord(file, ["branches", "branchCoverage", "branch_coverage"])),
|
|
197
194
|
functions: toNumber(
|
|
198
|
-
valueFromRecord(file, ["functions", "functionCoverage", "function_coverage"])
|
|
195
|
+
valueFromRecord(file, ["functions", "functionCoverage", "function_coverage"]),
|
|
199
196
|
),
|
|
200
197
|
uncoveredFunctions: parseUncoveredFunctions(
|
|
201
|
-
valueFromRecord(file, ["uncoveredFunctions", "uncovered_functions"])
|
|
198
|
+
valueFromRecord(file, ["uncoveredFunctions", "uncovered_functions"]),
|
|
202
199
|
),
|
|
203
|
-
}
|
|
200
|
+
}
|
|
204
201
|
}
|
|
205
202
|
|
|
206
203
|
function parseCoverage(payload: CoveragePayload): { files: ForgeCoverageFile[] } {
|
|
@@ -209,32 +206,33 @@ function parseCoverage(payload: CoveragePayload): { files: ForgeCoverageFile[] }
|
|
|
209
206
|
files: payload.files
|
|
210
207
|
.filter((item): item is Record<string, unknown> => !!item && typeof item === "object")
|
|
211
208
|
.map((item) => normalizeCoverageFile(item)),
|
|
212
|
-
}
|
|
209
|
+
}
|
|
213
210
|
}
|
|
214
211
|
|
|
215
212
|
if (payload.coverage && typeof payload.coverage === "object") {
|
|
216
|
-
const files: ForgeCoverageFile[] = []
|
|
213
|
+
const files: ForgeCoverageFile[] = []
|
|
217
214
|
for (const [path, metrics] of Object.entries(payload.coverage)) {
|
|
218
215
|
if (!metrics || typeof metrics !== "object") {
|
|
219
|
-
continue
|
|
216
|
+
continue
|
|
220
217
|
}
|
|
221
218
|
files.push(
|
|
222
219
|
normalizeCoverageFile({
|
|
223
220
|
path,
|
|
224
221
|
...metrics,
|
|
225
|
-
})
|
|
226
|
-
)
|
|
222
|
+
}),
|
|
223
|
+
)
|
|
227
224
|
}
|
|
228
225
|
|
|
229
|
-
return { files }
|
|
226
|
+
return { files }
|
|
230
227
|
}
|
|
231
228
|
|
|
232
|
-
return { files: [] }
|
|
229
|
+
return { files: [] }
|
|
233
230
|
}
|
|
234
231
|
|
|
235
|
-
function normalizeArgs(args: ForgeTestArgs): NormalizedForgeTestArgs {
|
|
232
|
+
function normalizeArgs(args: ForgeTestArgs, context: ToolContext): NormalizedForgeTestArgs {
|
|
233
|
+
const target = args.target && args.target !== "." ? args.target : resolveProjectDir(context)
|
|
236
234
|
return {
|
|
237
|
-
target
|
|
235
|
+
target,
|
|
238
236
|
match_test: args.match_test,
|
|
239
237
|
match_contract: args.match_contract,
|
|
240
238
|
fork_url: args.fork_url,
|
|
@@ -244,26 +242,26 @@ function normalizeArgs(args: ForgeTestArgs): NormalizedForgeTestArgs {
|
|
|
244
242
|
: 3,
|
|
245
243
|
gas_report: args.gas_report,
|
|
246
244
|
coverage: args.coverage ?? false,
|
|
247
|
-
}
|
|
245
|
+
}
|
|
248
246
|
}
|
|
249
247
|
|
|
250
248
|
function buildForgeTestCommand(args: NormalizedForgeTestArgs): string[] {
|
|
251
|
-
const command = ["forge", "test", "--json", `-v${"v".repeat(args.verbosity - 1)}`]
|
|
249
|
+
const command = ["forge", "test", "--json", `-v${"v".repeat(args.verbosity - 1)}`]
|
|
252
250
|
|
|
253
251
|
if (args.match_test) {
|
|
254
|
-
command.push("--match-test", args.match_test)
|
|
252
|
+
command.push("--match-test", args.match_test)
|
|
255
253
|
}
|
|
256
254
|
if (args.match_contract) {
|
|
257
|
-
command.push("--match-contract", args.match_contract)
|
|
255
|
+
command.push("--match-contract", args.match_contract)
|
|
258
256
|
}
|
|
259
257
|
if (args.fork_url) {
|
|
260
|
-
command.push("--fork-url", args.fork_url)
|
|
258
|
+
command.push("--fork-url", args.fork_url)
|
|
261
259
|
}
|
|
262
260
|
if (args.gas_report) {
|
|
263
|
-
command.push("--gas-report")
|
|
261
|
+
command.push("--gas-report")
|
|
264
262
|
}
|
|
265
263
|
|
|
266
|
-
return command
|
|
264
|
+
return command
|
|
267
265
|
}
|
|
268
266
|
|
|
269
267
|
const runForgeCommand: RunForgeCommand = async (command, signal, cwd) => {
|
|
@@ -272,29 +270,29 @@ const runForgeCommand: RunForgeCommand = async (command, signal, cwd) => {
|
|
|
272
270
|
stdout: "pipe",
|
|
273
271
|
stderr: "pipe",
|
|
274
272
|
signal,
|
|
275
|
-
})
|
|
273
|
+
})
|
|
276
274
|
|
|
277
275
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
278
276
|
child.exited,
|
|
279
277
|
new Response(child.stdout).text(),
|
|
280
278
|
new Response(child.stderr).text(),
|
|
281
|
-
])
|
|
279
|
+
])
|
|
282
280
|
|
|
283
281
|
return {
|
|
284
282
|
stdout,
|
|
285
283
|
stderr,
|
|
286
284
|
exitCode,
|
|
287
|
-
}
|
|
288
|
-
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
289
287
|
|
|
290
288
|
export async function executeForgeTest(
|
|
291
289
|
args: ForgeTestArgs,
|
|
292
290
|
context: ToolContext,
|
|
293
|
-
runCommand: RunForgeCommand = runForgeCommand
|
|
291
|
+
runCommand: RunForgeCommand = runForgeCommand,
|
|
294
292
|
): Promise<ForgeTestResult> {
|
|
295
|
-
const startedAt = Date.now()
|
|
296
|
-
const normalizedArgs = normalizeArgs(args)
|
|
297
|
-
context.metadata({ title: `Run forge test: ${normalizedArgs.target}` })
|
|
293
|
+
const startedAt = Date.now()
|
|
294
|
+
const normalizedArgs = normalizeArgs(args, context)
|
|
295
|
+
context.metadata({ title: `Run forge test: ${normalizedArgs.target}` })
|
|
298
296
|
|
|
299
297
|
const fail = (error: string): ForgeTestResult => ({
|
|
300
298
|
success: false,
|
|
@@ -302,23 +300,23 @@ export async function executeForgeTest(
|
|
|
302
300
|
tests: [],
|
|
303
301
|
executionTime: Date.now() - startedAt,
|
|
304
302
|
error,
|
|
305
|
-
})
|
|
303
|
+
})
|
|
306
304
|
|
|
307
305
|
try {
|
|
308
306
|
const testResult = await runCommand(
|
|
309
307
|
buildForgeTestCommand(normalizedArgs),
|
|
310
308
|
context.abort,
|
|
311
|
-
normalizedArgs.target
|
|
312
|
-
)
|
|
309
|
+
normalizedArgs.target,
|
|
310
|
+
)
|
|
313
311
|
|
|
314
|
-
let payload: ForgeTestPayload
|
|
312
|
+
let payload: ForgeTestPayload
|
|
315
313
|
try {
|
|
316
|
-
payload = JSON.parse(testResult.stdout) as ForgeTestPayload
|
|
314
|
+
payload = JSON.parse(testResult.stdout) as ForgeTestPayload
|
|
317
315
|
} catch {
|
|
318
|
-
return fail("Invalid JSON output from forge test")
|
|
316
|
+
return fail("Invalid JSON output from forge test")
|
|
319
317
|
}
|
|
320
318
|
|
|
321
|
-
const parsed = parseTests(payload)
|
|
319
|
+
const parsed = parseTests(payload)
|
|
322
320
|
const output: ForgeTestResult = {
|
|
323
321
|
success:
|
|
324
322
|
testResult.exitCode === 0 &&
|
|
@@ -327,55 +325,53 @@ export async function executeForgeTest(
|
|
|
327
325
|
summary: parsed.summary,
|
|
328
326
|
tests: parsed.tests,
|
|
329
327
|
executionTime: Date.now() - startedAt,
|
|
330
|
-
}
|
|
328
|
+
}
|
|
331
329
|
|
|
332
|
-
const gasReport = payload.gas_report ?? payload.gasReport
|
|
330
|
+
const gasReport = payload.gas_report ?? payload.gasReport
|
|
333
331
|
if (gasReport) {
|
|
334
|
-
output.gasReport = gasReport
|
|
332
|
+
output.gasReport = gasReport
|
|
335
333
|
}
|
|
336
334
|
|
|
337
335
|
if (normalizedArgs.coverage) {
|
|
338
336
|
const coverageResult = await runCommand(
|
|
339
337
|
["forge", "coverage", "--report", "json"],
|
|
340
338
|
context.abort,
|
|
341
|
-
normalizedArgs.target
|
|
342
|
-
)
|
|
339
|
+
normalizedArgs.target,
|
|
340
|
+
)
|
|
343
341
|
if (coverageResult.exitCode !== 0) {
|
|
344
|
-
output.error = coverageResult.stderr.trim() || "forge coverage failed"
|
|
345
|
-
output.success = false
|
|
342
|
+
output.error = coverageResult.stderr.trim() || "forge coverage failed"
|
|
343
|
+
output.success = false
|
|
346
344
|
} else {
|
|
347
345
|
try {
|
|
348
|
-
const coveragePayload = JSON.parse(coverageResult.stdout) as CoveragePayload
|
|
349
|
-
output.coverageReport = parseCoverage(coveragePayload)
|
|
346
|
+
const coveragePayload = JSON.parse(coverageResult.stdout) as CoveragePayload
|
|
347
|
+
output.coverageReport = parseCoverage(coveragePayload)
|
|
350
348
|
} catch {
|
|
351
|
-
output.error = "Invalid JSON output from forge coverage"
|
|
352
|
-
output.success = false
|
|
349
|
+
output.error = "Invalid JSON output from forge coverage"
|
|
350
|
+
output.success = false
|
|
353
351
|
}
|
|
354
352
|
}
|
|
355
353
|
}
|
|
356
354
|
|
|
357
355
|
if (testResult.exitCode !== 0 && !output.error) {
|
|
358
|
-
output.error =
|
|
356
|
+
output.error =
|
|
357
|
+
testResult.stderr.trim() || `forge test exited with code ${testResult.exitCode}`
|
|
359
358
|
}
|
|
360
359
|
|
|
361
|
-
return output
|
|
360
|
+
return output
|
|
362
361
|
} catch (error) {
|
|
363
362
|
if (context.abort.aborted || (error instanceof DOMException && error.name === "AbortError")) {
|
|
364
|
-
return fail("forge test aborted")
|
|
363
|
+
return fail("forge test aborted")
|
|
365
364
|
}
|
|
366
365
|
|
|
367
|
-
const maybeError = error as Error & { code?: string }
|
|
366
|
+
const maybeError = error as Error & { code?: string }
|
|
368
367
|
if (maybeError.code === "ENOENT") {
|
|
369
|
-
return fail("Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash")
|
|
368
|
+
return fail("Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash")
|
|
370
369
|
}
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
maybeError.message.toLowerCase().includes("timed out")
|
|
374
|
-
) {
|
|
375
|
-
return fail("forge test timed out");
|
|
370
|
+
if (maybeError.code === "ETIMEDOUT" || maybeError.message.toLowerCase().includes("timed out")) {
|
|
371
|
+
return fail("forge test timed out")
|
|
376
372
|
}
|
|
377
373
|
|
|
378
|
-
return fail(maybeError.message || "forge test failed")
|
|
374
|
+
return fail(maybeError.message || "forge test failed")
|
|
379
375
|
}
|
|
380
376
|
}
|
|
381
377
|
|
|
@@ -391,7 +387,7 @@ export const forgeTestTool = tool({
|
|
|
391
387
|
coverage: tool.schema.boolean().default(false),
|
|
392
388
|
},
|
|
393
389
|
async execute(args, context) {
|
|
394
|
-
const result = await executeForgeTest(args, context)
|
|
395
|
-
return JSON.stringify(result)
|
|
390
|
+
const result = await executeForgeTest(args, context)
|
|
391
|
+
return JSON.stringify(result)
|
|
396
392
|
},
|
|
397
|
-
})
|
|
393
|
+
})
|