skills/bap578-testing/SKILL.md
Use this skill when writing, running, debugging, or extending BAP-578 Non-Fungible Agent tests, including infrastructure setup, function coverage, edge cases, upgrades, gas profiling, and Hardhat workflows.
npx skillsauth add chatandbuild/skills-repo BAP-578 TestingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill to write, run, debug, and extend the test suite for BAP-578 Non-Fungible Agents. This covers test infrastructure setup, comprehensive test cases for every contract function, edge case coverage, upgrade testing, gas profiling, and debugging strategies.
The testing layer is the quality gate for the entire contract. Tests prove that every function behaves correctly, every access control is enforced, every edge case is handled, and every upgrade preserves state. Without comprehensive tests, the contract cannot be trusted for deployment.
The test suite remembers every expected behavior: valid minting flows, fee calculations, free mint restrictions, transfer rules, withdrawal safety, status toggles, metadata updates, and admin operations. Each test case is a documented expectation of how the contract should work.
Tests map directly to contract functions and user stories. Every test has a clear description, setup, action, and assertion. Tests run against a local Hardhat network with deterministic results. Coverage tools can verify that all code paths are exercised.
cd non-fungible-agents-BAP-578
npm install
# Run all tests
npm test
# Run with verbose output
npx hardhat test --verbose
# Run a specific test file
npx hardhat test test/NonFungibleAgents.test.js
# Run with gas reporting
REPORT_GAS=true npx hardhat test
# Run with coverage
npx hardhat coverage
test/
├── NonFungibleAgents.test.js # Main test suite
├── helpers/
│ ├── fixtures.js # Reusable deployment fixtures
│ └── constants.js # Test constants
└── upgrade/
└── upgrade.test.js # Upgrade-specific tests
const { ethers, upgrades } = require("hardhat");
async function deployFixture() {
const [owner, treasury, user1, user2, user3] = await ethers.getSigners();
const NFA = await ethers.getContractFactory("NonFungibleAgents");
const nfa = await upgrades.deployProxy(NFA, [
"Non-Fungible Agents",
"NFA",
treasury.address,
], { kind: "uups" });
const MINT_FEE = ethers.parseEther("0.01");
const DEFAULT_FREE_MINTS = 3;
const defaultMetadata = {
persona: JSON.stringify({ name: "TestAgent", traits: ["helpful"] }),
experience: "Test agent for unit testing",
voiceHash: "",
animationURI: "",
vaultURI: "",
vaultHash: ethers.ZeroHash,
};
return {
nfa,
owner,
treasury,
user1,
user2,
user3,
MINT_FEE,
DEFAULT_FREE_MINTS,
defaultMetadata,
};
}
module.exports = { deployFixture };
async function deployWithAgentsFixture() {
const base = await deployFixture();
const { nfa, user1, user2, defaultMetadata } = base;
// Mint 3 free agents for user1
for (let i = 0; i < 3; i++) {
await nfa.connect(user1).createAgent(
user1.address,
ethers.ZeroAddress,
`ipfs://meta${i}`,
{ ...defaultMetadata, experience: `Agent ${i}` }
);
}
// Mint 1 paid agent for user2
await nfa.connect(user2).createAgent(
user2.address,
ethers.ZeroAddress,
"ipfs://meta-paid",
defaultMetadata,
{ value: base.MINT_FEE }
);
return { ...base, user1AgentIds: [1, 2, 3], user2AgentId: 4 };
}
describe("createAgent", function () {
describe("Free mints", function () {
it("allows free mint to self", async function () {
const { nfa, user1, defaultMetadata } = await deployFixture();
await expect(
nfa.connect(user1).createAgent(
user1.address,
ethers.ZeroAddress,
"ipfs://meta",
defaultMetadata
)
).to.emit(nfa, "AgentCreated");
});
it("reverts free mint to different address", async function () {
const { nfa, user1, user2, defaultMetadata } = await deployFixture();
await expect(
nfa.connect(user1).createAgent(
user2.address,
ethers.ZeroAddress,
"ipfs://meta",
defaultMetadata
)
).to.be.revertedWith("Free mints must be to self");
});
it("tracks free mint count per user", async function () {
const { nfa, user1, defaultMetadata } = await deployFixture();
const initialFree = await nfa.getFreeMints(user1.address);
await nfa.connect(user1).createAgent(
user1.address, ethers.ZeroAddress, "ipfs://meta", defaultMetadata
);
const afterFree = await nfa.getFreeMints(user1.address);
expect(afterFree).to.equal(initialFree - 1n);
});
it("marks free-minted tokens as non-transferable", async function () {
const { nfa, user1, user2, defaultMetadata } = await deployFixture();
await nfa.connect(user1).createAgent(
user1.address, ethers.ZeroAddress, "ipfs://meta", defaultMetadata
);
expect(await nfa.isFreeMint(1)).to.be.true;
await expect(
nfa.connect(user1).transferFrom(user1.address, user2.address, 1)
).to.be.reverted;
});
it("exhausts free mints after allocation", async function () {
const { nfa, user1, defaultMetadata, DEFAULT_FREE_MINTS } = await deployFixture();
for (let i = 0; i < DEFAULT_FREE_MINTS; i++) {
await nfa.connect(user1).createAgent(
user1.address, ethers.ZeroAddress, `ipfs://meta${i}`, defaultMetadata
);
}
expect(await nfa.getFreeMints(user1.address)).to.equal(0);
});
});
describe("Paid mints", function () {
it("requires exact fee after free mints exhausted", async function () {
const { nfa, user1, defaultMetadata, MINT_FEE, DEFAULT_FREE_MINTS } = await deployFixture();
// Exhaust free mints
for (let i = 0; i < DEFAULT_FREE_MINTS; i++) {
await nfa.connect(user1).createAgent(
user1.address, ethers.ZeroAddress, `ipfs://meta${i}`, defaultMetadata
);
}
// Paid mint with correct fee
await expect(
nfa.connect(user1).createAgent(
user1.address, ethers.ZeroAddress, "ipfs://paid", defaultMetadata,
{ value: MINT_FEE }
)
).to.emit(nfa, "AgentCreated");
});
it("reverts with incorrect fee", async function () {
const { nfa, user1, defaultMetadata, DEFAULT_FREE_MINTS } = await deployFixture();
for (let i = 0; i < DEFAULT_FREE_MINTS; i++) {
await nfa.connect(user1).createAgent(
user1.address, ethers.ZeroAddress, `ipfs://meta${i}`, defaultMetadata
);
}
await expect(
nfa.connect(user1).createAgent(
user1.address, ethers.ZeroAddress, "ipfs://paid", defaultMetadata,
{ value: ethers.parseEther("0.005") }
)
).to.be.revertedWith("Incorrect fee");
});
it("allows paid mint to different address", async function () {
const { nfa, user1, user2, defaultMetadata, MINT_FEE, DEFAULT_FREE_MINTS } = await deployFixture();
for (let i = 0; i < DEFAULT_FREE_MINTS; i++) {
await nfa.connect(user1).createAgent(
user1.address, ethers.ZeroAddress, `ipfs://meta${i}`, defaultMetadata
);
}
await nfa.connect(user1).createAgent(
user2.address, ethers.ZeroAddress, "ipfs://gift", defaultMetadata,
{ value: MINT_FEE }
);
expect(await nfa.ownerOf(DEFAULT_FREE_MINTS + 1)).to.equal(user2.address);
});
it("sends fee to treasury", async function () {
const { nfa, user1, treasury, defaultMetadata, MINT_FEE, DEFAULT_FREE_MINTS } = await deployFixture();
for (let i = 0; i < DEFAULT_FREE_MINTS; i++) {
await nfa.connect(user1).createAgent(
user1.address, ethers.ZeroAddress, `ipfs://meta${i}`, defaultMetadata
);
}
await expect(
nfa.connect(user1).createAgent(
user1.address, ethers.ZeroAddress, "ipfs://paid", defaultMetadata,
{ value: MINT_FEE }
)
).to.changeEtherBalance(treasury, MINT_FEE);
});
it("paid tokens are transferable", async function () {
const { nfa, user1, user2, defaultMetadata, MINT_FEE, DEFAULT_FREE_MINTS } = await deployFixture();
for (let i = 0; i < DEFAULT_FREE_MINTS; i++) {
await nfa.connect(user1).createAgent(
user1.address, ethers.ZeroAddress, `ipfs://meta${i}`, defaultMetadata
);
}
const tokenId = DEFAULT_FREE_MINTS + 1;
await nfa.connect(user1).createAgent(
user1.address, ethers.ZeroAddress, "ipfs://paid", defaultMetadata,
{ value: MINT_FEE }
);
expect(await nfa.isFreeMint(tokenId)).to.be.false;
await nfa.connect(user1).transferFrom(user1.address, user2.address, tokenId);
expect(await nfa.ownerOf(tokenId)).to.equal(user2.address);
});
});
});
describe("Agent funding", function () {
it("funds an agent with BNB", async function () {
const { nfa, user1 } = await deployWithAgentsFixture();
const amount = ethers.parseEther("0.5");
await expect(
nfa.connect(user1).fundAgent(1, { value: amount })
).to.emit(nfa, "AgentFunded").withArgs(1, user1.address, amount);
const state = await nfa.getAgentState(1);
expect(state.balance).to.equal(amount);
});
it("allows anyone to fund any agent", async function () {
const { nfa, user2 } = await deployWithAgentsFixture();
const amount = ethers.parseEther("0.1");
await nfa.connect(user2).fundAgent(1, { value: amount });
const state = await nfa.getAgentState(1);
expect(state.balance).to.equal(amount);
});
});
describe("Agent withdrawal", function () {
it("allows token owner to withdraw", async function () {
const { nfa, user1 } = await deployWithAgentsFixture();
const fundAmount = ethers.parseEther("1.0");
const withdrawAmount = ethers.parseEther("0.5");
await nfa.connect(user1).fundAgent(1, { value: fundAmount });
await expect(
nfa.connect(user1).withdrawFromAgent(1, withdrawAmount)
).to.changeEtherBalance(user1, withdrawAmount);
const state = await nfa.getAgentState(1);
expect(state.balance).to.equal(fundAmount - withdrawAmount);
});
it("reverts withdrawal by non-owner", async function () {
const { nfa, user1, user2 } = await deployWithAgentsFixture();
await nfa.connect(user1).fundAgent(1, { value: ethers.parseEther("1.0") });
await expect(
nfa.connect(user2).withdrawFromAgent(1, ethers.parseEther("0.5"))
).to.be.reverted;
});
it("reverts withdrawal exceeding balance", async function () {
const { nfa, user1 } = await deployWithAgentsFixture();
await nfa.connect(user1).fundAgent(1, { value: ethers.parseEther("0.5") });
await expect(
nfa.connect(user1).withdrawFromAgent(1, ethers.parseEther("1.0"))
).to.be.reverted;
});
});
describe("Access control", function () {
it("only owner can set treasury", async function () {
const { nfa, user1 } = await deployFixture();
await expect(
nfa.connect(user1).setTreasury(user1.address)
).to.be.reverted;
});
it("only owner can pause", async function () {
const { nfa, user1 } = await deployFixture();
await expect(
nfa.connect(user1).setPaused(true)
).to.be.reverted;
});
it("only owner can grant free mints", async function () {
const { nfa, user1, user2 } = await deployFixture();
await expect(
nfa.connect(user1).grantAdditionalFreeMints(user2.address, 5)
).to.be.reverted;
});
it("only token owner can set agent status", async function () {
const { nfa, user2 } = await deployWithAgentsFixture();
await expect(
nfa.connect(user2).setAgentStatus(1, false)
).to.be.reverted;
});
it("only token owner can set logic address", async function () {
const { nfa, user2 } = await deployWithAgentsFixture();
await expect(
nfa.connect(user2).setLogicAddress(1, ethers.ZeroAddress)
).to.be.reverted;
});
it("only token owner can update metadata", async function () {
const { nfa, user2, defaultMetadata } = await deployWithAgentsFixture();
await expect(
nfa.connect(user2).updateAgentMetadata(1, "ipfs://new", defaultMetadata)
).to.be.reverted;
});
});
describe("Agent status", function () {
it("toggles agent status", async function () {
const { nfa, user1 } = await deployWithAgentsFixture();
await nfa.connect(user1).setAgentStatus(1, false);
let state = await nfa.getAgentState(1);
expect(state.active).to.be.false;
await nfa.connect(user1).setAgentStatus(1, true);
state = await nfa.getAgentState(1);
expect(state.active).to.be.true;
});
it("emits AgentStatusChanged event", async function () {
const { nfa, user1 } = await deployWithAgentsFixture();
await expect(
nfa.connect(user1).setAgentStatus(1, false)
).to.emit(nfa, "AgentStatusChanged").withArgs(1, false);
});
});
describe("Metadata updates", function () {
it("updates agent metadata", async function () {
const { nfa, user1 } = await deployWithAgentsFixture();
const newMetadata = {
persona: JSON.stringify({ name: "Updated" }),
experience: "Updated experience",
voiceHash: "newvoice",
animationURI: "ipfs://newanim",
vaultURI: "ipfs://newvault",
vaultHash: ethers.keccak256(ethers.toUtf8Bytes("vault content")),
};
await nfa.connect(user1).updateAgentMetadata(1, "ipfs://newuri", newMetadata);
const meta = await nfa.getAgentMetadata(1);
expect(meta.experience).to.equal("Updated experience");
expect(meta.vaultURI).to.equal("ipfs://newvault");
});
it("emits MetadataUpdated event", async function () {
const { nfa, user1, defaultMetadata } = await deployWithAgentsFixture();
await expect(
nfa.connect(user1).updateAgentMetadata(1, "ipfs://new", defaultMetadata)
).to.emit(nfa, "MetadataUpdated").withArgs(1, "ipfs://new");
});
});
msg.value; check if free mints remainconnect(correctUser)setPaused(false) in setup--verbose flag).require or custom error is triggered.console.log in the contract temporarily for deeper debugging.console.sol import for in-contract logging.REPORT_GAS=true npx hardhat test
This outputs a table showing gas usage per function call. Use it to:
it("mint gas should be under 400k", async function () {
const tx = await nfa.connect(user1).createAgent(
user1.address, ethers.ZeroAddress, "ipfs://meta", defaultMetadata
);
const receipt = await tx.wait();
expect(receipt.gasUsed).to.be.lt(400000n);
});
npx hardhat coverage
Generates an HTML report in coverage/index.html. Target metrics:
| Category | Target | |----------|--------| | Statements | > 95% | | Branches | > 90% | | Functions | 100% | | Lines | > 95% |
Review uncovered lines to identify missing test cases. Pay special attention to:
When asked for testing help, respond with:
bap578bap578-upgradebap578-security-auditdocumentation
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.
development
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
devops
Deploy applications and infrastructure to Cloudflare using Workers, Pages, and related platform services. Use when the user asks to deploy, host, publish, or set up a project on Cloudflare.
tools
Use this skill when designing and building durable command-line tools from API docs, OpenAPI specs, SDKs, curl examples, admin tools, web apps, or local scripts, especially when the CLI should expose composable commands, stable JSON output, auth/config handling, install-on-PATH behavior, and a companion skill.