-
Notifications
You must be signed in to change notification settings - Fork 17
test: finops + tool-lookup — RBAC formatting and Zod schema introspection #397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
anandgupta42
wants to merge
1
commit into
main
from
claude/test-finops-tool-lookup-session_01PnbJpBRPw8DrqSBDxhrCjg
+397
−0
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
248 changes: 248 additions & 0 deletions
248
packages/opencode/test/altimate/finops-role-access.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,248 @@ | ||
| /** | ||
| * Tests for finops role-access formatting functions: | ||
| * formatGrants, formatHierarchy, formatUserRoles. | ||
| * | ||
| * These render RBAC data as markdown tables. Incorrect output | ||
| * could cause data engineers to miss security issues during audits. | ||
| * Tests use Dispatcher.call spying to supply known RBAC data. | ||
| */ | ||
| import { describe, test, expect, spyOn, afterAll, beforeEach } from "bun:test" | ||
| import * as Dispatcher from "../../src/altimate/native/dispatcher" | ||
| import { | ||
| FinopsRoleGrantsTool, | ||
| FinopsRoleHierarchyTool, | ||
| FinopsUserRolesTool, | ||
| } from "../../src/altimate/tools/finops-role-access" | ||
| import { SessionID, MessageID } from "../../src/session/schema" | ||
|
|
||
| beforeEach(() => { | ||
| process.env.ALTIMATE_TELEMETRY_DISABLED = "true" | ||
| }) | ||
| afterAll(() => { | ||
| delete process.env.ALTIMATE_TELEMETRY_DISABLED | ||
| }) | ||
|
|
||
| const ctx = { | ||
| sessionID: SessionID.make("ses_test"), | ||
| messageID: MessageID.make("msg_test"), | ||
| callID: "call_test", | ||
| agent: "build", | ||
| abort: AbortSignal.any([]), | ||
| messages: [], | ||
| metadata: () => {}, | ||
| ask: async () => {}, | ||
| } | ||
|
|
||
| let dispatcherSpy: ReturnType<typeof spyOn> | ||
|
|
||
| function mockDispatcher(responses: Record<string, any>) { | ||
| dispatcherSpy?.mockRestore() | ||
| dispatcherSpy = spyOn(Dispatcher, "call").mockImplementation(async (method: string) => { | ||
| if (responses[method]) return responses[method] | ||
| throw new Error(`No mock for ${method}`) | ||
| }) | ||
| } | ||
|
|
||
| afterAll(() => { | ||
| dispatcherSpy?.mockRestore() | ||
| }) | ||
|
|
||
| describe("formatGrants: privilege summary and grant rows", () => { | ||
| test("renders privilege summary and grant table with standard Snowflake fields", async () => { | ||
| mockDispatcher({ | ||
| "finops.role_grants": { | ||
| success: true, | ||
| grant_count: 2, | ||
| privilege_summary: { SELECT: 2, INSERT: 1 }, | ||
| grants: [ | ||
| { grantee_name: "ANALYST", privilege: "SELECT", object_type: "TABLE", object_name: "orders" }, | ||
| { grantee_name: "ADMIN", privilege: "INSERT", object_type: "TABLE", object_name: "users" }, | ||
| ], | ||
| }, | ||
| }) | ||
|
|
||
| const tool = await FinopsRoleGrantsTool.init() | ||
| const result = await tool.execute({ warehouse: "test_wh", limit: 100 }, ctx as any) | ||
|
|
||
| expect(result.title).toContain("2 found") | ||
| expect(result.output).toContain("Privilege Summary") | ||
| expect(result.output).toContain("SELECT: 2") | ||
| expect(result.output).toContain("INSERT: 1") | ||
| expect(result.output).toContain("ANALYST | SELECT | TABLE | orders") | ||
| expect(result.output).toContain("ADMIN | INSERT | TABLE | users") | ||
| }) | ||
|
|
||
| test("uses fallback field aliases (role, granted_on, name)", async () => { | ||
| mockDispatcher({ | ||
| "finops.role_grants": { | ||
| success: true, | ||
| grant_count: 1, | ||
| privilege_summary: {}, | ||
| grants: [ | ||
| { role: "DBA", privilege: "USAGE", granted_on: "WAREHOUSE", name: "compute_wh" }, | ||
| ], | ||
| }, | ||
| }) | ||
|
|
||
| const tool = await FinopsRoleGrantsTool.init() | ||
| const result = await tool.execute({ warehouse: "test_wh", limit: 100 }, ctx as any) | ||
|
|
||
| // formatGrants should fall back to r.role, r.granted_on, r.name | ||
| expect(result.output).toContain("DBA | USAGE | WAREHOUSE | compute_wh") | ||
| }) | ||
|
|
||
| test("handles empty grants array", async () => { | ||
| mockDispatcher({ | ||
| "finops.role_grants": { | ||
| success: true, | ||
| grant_count: 0, | ||
| privilege_summary: {}, | ||
| grants: [], | ||
| }, | ||
| }) | ||
|
|
||
| const tool = await FinopsRoleGrantsTool.init() | ||
| const result = await tool.execute({ warehouse: "test_wh", limit: 100 }, ctx as any) | ||
|
|
||
| expect(result.output).toContain("No grants found") | ||
| }) | ||
|
|
||
| test("returns error message on Dispatcher failure", async () => { | ||
| mockDispatcher({ | ||
| "finops.role_grants": { | ||
| success: false, | ||
| error: "Connection refused", | ||
| }, | ||
| }) | ||
|
|
||
| const tool = await FinopsRoleGrantsTool.init() | ||
| const result = await tool.execute({ warehouse: "test_wh", limit: 100 }, ctx as any) | ||
|
|
||
| expect(result.title).toContain("FAILED") | ||
| expect(result.output).toContain("Connection refused") | ||
| }) | ||
| }) | ||
|
|
||
| describe("formatHierarchy: recursive role tree rendering", () => { | ||
| test("renders two-level nested hierarchy with children key", async () => { | ||
| mockDispatcher({ | ||
| "finops.role_hierarchy": { | ||
| success: true, | ||
| role_count: 3, | ||
| hierarchy: [ | ||
| { | ||
| name: "SYSADMIN", | ||
| children: [ | ||
| { name: "DBA", children: [] }, | ||
| { name: "ANALYST", children: [] }, | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
| }) | ||
|
|
||
| const tool = await FinopsRoleHierarchyTool.init() | ||
| const result = await tool.execute({ warehouse: "test_wh" }, ctx as any) | ||
|
|
||
| expect(result.title).toContain("3 roles") | ||
| expect(result.output).toContain("Role Hierarchy") | ||
| expect(result.output).toContain("SYSADMIN") | ||
| expect(result.output).toContain("-> DBA") | ||
| expect(result.output).toContain("-> ANALYST") | ||
| }) | ||
|
|
||
| test("uses granted_roles fallback alias for children", async () => { | ||
| mockDispatcher({ | ||
| "finops.role_hierarchy": { | ||
| success: true, | ||
| role_count: 2, | ||
| hierarchy: [ | ||
| { | ||
| role: "ACCOUNTADMIN", | ||
| granted_roles: [{ role: "SECURITYADMIN" }], | ||
| }, | ||
| ], | ||
| }, | ||
| }) | ||
|
|
||
| const tool = await FinopsRoleHierarchyTool.init() | ||
| const result = await tool.execute({ warehouse: "test_wh" }, ctx as any) | ||
|
|
||
| // Should use r.role as name and r.granted_roles as children | ||
| expect(result.output).toContain("ACCOUNTADMIN") | ||
| expect(result.output).toContain("-> SECURITYADMIN") | ||
| }) | ||
|
|
||
| test("handles empty hierarchy", async () => { | ||
| mockDispatcher({ | ||
| "finops.role_hierarchy": { | ||
| success: true, | ||
| role_count: 0, | ||
| hierarchy: [], | ||
| }, | ||
| }) | ||
|
|
||
| const tool = await FinopsRoleHierarchyTool.init() | ||
| const result = await tool.execute({ warehouse: "test_wh" }, ctx as any) | ||
|
|
||
| expect(result.output).toContain("Role Hierarchy") | ||
| // No roles rendered but header is present | ||
| expect(result.output).not.toContain("->") | ||
| }) | ||
| }) | ||
|
|
||
| describe("formatUserRoles: user-role assignment table", () => { | ||
| test("renders user assignments with standard fields", async () => { | ||
| mockDispatcher({ | ||
| "finops.user_roles": { | ||
| success: true, | ||
| assignment_count: 2, | ||
| assignments: [ | ||
| { grantee_name: "alice@corp.com", role: "ANALYST", granted_by: "SECURITYADMIN" }, | ||
| { grantee_name: "bob@corp.com", role: "DBA", granted_by: "ACCOUNTADMIN" }, | ||
| ], | ||
| }, | ||
| }) | ||
|
|
||
| const tool = await FinopsUserRolesTool.init() | ||
| const result = await tool.execute({ warehouse: "test_wh", limit: 100 }, ctx as any) | ||
|
|
||
| expect(result.title).toContain("2 assignments") | ||
| expect(result.output).toContain("User Role Assignments") | ||
| expect(result.output).toContain("alice@corp.com | ANALYST | SECURITYADMIN") | ||
| expect(result.output).toContain("bob@corp.com | DBA | ACCOUNTADMIN") | ||
| }) | ||
|
|
||
| test("uses fallback aliases (user_name, role_name, grantor)", async () => { | ||
| mockDispatcher({ | ||
| "finops.user_roles": { | ||
| success: true, | ||
| assignment_count: 1, | ||
| assignments: [ | ||
| { user_name: "charlie", role_name: "READER", grantor: "ADMIN" }, | ||
| ], | ||
| }, | ||
| }) | ||
|
|
||
| const tool = await FinopsUserRolesTool.init() | ||
| const result = await tool.execute({ warehouse: "test_wh", limit: 100 }, ctx as any) | ||
|
|
||
| // Falls back to r.user_name (via user fallback chain), r.role_name, r.grantor | ||
| expect(result.output).toContain("charlie | READER | ADMIN") | ||
| }) | ||
|
|
||
| test("handles empty assignments", async () => { | ||
| mockDispatcher({ | ||
| "finops.user_roles": { | ||
| success: true, | ||
| assignment_count: 0, | ||
| assignments: [], | ||
| }, | ||
| }) | ||
|
|
||
| const tool = await FinopsUserRolesTool.init() | ||
| const result = await tool.execute({ warehouse: "test_wh", limit: 100 }, ctx as any) | ||
|
|
||
| expect(result.output).toContain("No user role assignments found") | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| /** | ||
| * Tests for the tool_lookup tool — Zod schema introspection | ||
| * (describeZodSchema, inferZodType, unwrap, getShape). | ||
| * | ||
| * The agent uses tool_lookup to discover other tools' parameter contracts. | ||
| * Incorrect introspection leads to wrong tool calls. | ||
| */ | ||
| import { describe, test, expect, beforeEach, afterAll } from "bun:test" | ||
| import z from "zod" | ||
| import { ToolLookupTool } from "../../src/altimate/tools/tool-lookup" | ||
| import { ToolRegistry } from "../../src/tool/registry" | ||
| import { Instance } from "../../src/project/instance" | ||
| import { tmpdir } from "../fixture/fixture" | ||
| import { SessionID, MessageID } from "../../src/session/schema" | ||
|
|
||
| beforeEach(() => { | ||
| process.env.ALTIMATE_TELEMETRY_DISABLED = "true" | ||
| }) | ||
| afterAll(() => { | ||
| delete process.env.ALTIMATE_TELEMETRY_DISABLED | ||
| }) | ||
|
|
||
| const ctx = { | ||
| sessionID: SessionID.make("ses_test"), | ||
| messageID: MessageID.make("msg_test"), | ||
| callID: "call_test", | ||
| agent: "build", | ||
| abort: AbortSignal.any([]), | ||
| messages: [], | ||
| metadata: () => {}, | ||
| ask: async () => {}, | ||
| } | ||
|
|
||
| describe("ToolLookupTool: Zod schema introspection", () => { | ||
| test("returns parameter info for tool with mixed types", async () => { | ||
| await using tmp = await tmpdir() | ||
| await Instance.provide({ | ||
| directory: tmp.path, | ||
| fn: async () => { | ||
| const testTool = { | ||
| id: "__test_lookup_mixed", | ||
| init: async () => ({ | ||
| description: "Test tool with mixed params", | ||
| parameters: z.object({ | ||
| name: z.string().describe("The name"), | ||
| count: z.number().describe("How many"), | ||
| verbose: z.boolean().optional().describe("Enable verbosity"), | ||
| tags: z.array(z.string()).describe("Tags list"), | ||
| mode: z.enum(["fast", "slow"]).default("fast").describe("Execution mode"), | ||
| }), | ||
| execute: async () => ({ title: "", output: "", metadata: {} }), | ||
| }), | ||
| } | ||
| await ToolRegistry.register(testTool) | ||
|
|
||
| const tool = await ToolLookupTool.init() | ||
| const result = await tool.execute({ tool_name: "__test_lookup_mixed" }, ctx as any) | ||
|
|
||
| // Required string param | ||
| expect(result.output).toContain("name") | ||
| expect(result.output).toContain("string") | ||
| expect(result.output).toContain("required") | ||
| expect(result.output).toContain("The name") | ||
|
|
||
| // Number param | ||
| expect(result.output).toContain("count") | ||
| expect(result.output).toContain("number") | ||
|
|
||
| // Optional boolean | ||
| expect(result.output).toContain("verbose") | ||
| expect(result.output).toContain("optional") | ||
|
|
||
| // Array param | ||
| expect(result.output).toContain("tags") | ||
| expect(result.output).toContain("array") | ||
|
|
||
| // Enum with default — inferZodType unwraps default then hits enum | ||
| expect(result.output).toContain("mode") | ||
| expect(result.output).toContain("enum") | ||
| }, | ||
| }) | ||
| }) | ||
|
|
||
| test("returns 'Tool not found' with available tools list", async () => { | ||
| await using tmp = await tmpdir() | ||
| await Instance.provide({ | ||
| directory: tmp.path, | ||
| fn: async () => { | ||
| const tool = await ToolLookupTool.init() | ||
| const result = await tool.execute({ tool_name: "nonexistent_tool_xyz" }, ctx as any) | ||
| expect(result.title).toBe("Tool not found") | ||
| expect(result.output).toContain('No tool named "nonexistent_tool_xyz"') | ||
| expect(result.output).toContain("Available tools:") | ||
| }, | ||
| }) | ||
| }) | ||
|
|
||
| test("handles tool with empty parameters object", async () => { | ||
| await using tmp = await tmpdir() | ||
| await Instance.provide({ | ||
| directory: tmp.path, | ||
| fn: async () => { | ||
| const testTool = { | ||
| id: "__test_lookup_empty", | ||
| init: async () => ({ | ||
| description: "Tool with empty params", | ||
| parameters: z.object({}), | ||
| execute: async () => ({ title: "", output: "", metadata: {} }), | ||
| }), | ||
| } | ||
| await ToolRegistry.register(testTool) | ||
|
|
||
| const tool = await ToolLookupTool.init() | ||
| const result = await tool.execute({ tool_name: "__test_lookup_empty" }, ctx as any) | ||
| expect(result.output).toContain("No parameters") | ||
| }, | ||
| }) | ||
| }) | ||
|
|
||
| test("unwraps nested optional/default wrappers correctly", async () => { | ||
| await using tmp = await tmpdir() | ||
| await Instance.provide({ | ||
| directory: tmp.path, | ||
| fn: async () => { | ||
| const testTool = { | ||
| id: "__test_lookup_nested", | ||
| init: async () => ({ | ||
| description: "Tool with nested wrappers", | ||
| parameters: z.object({ | ||
| // default wrapping optional wrapping string | ||
| deep: z.string().optional().default("hello").describe("Deeply wrapped"), | ||
| }), | ||
| execute: async () => ({ title: "", output: "", metadata: {} }), | ||
| }), | ||
| } | ||
| await ToolRegistry.register(testTool) | ||
|
|
||
| const tool = await ToolLookupTool.init() | ||
| const result = await tool.execute({ tool_name: "__test_lookup_nested" }, ctx as any) | ||
|
|
||
| // Should unwrap to the inner string type | ||
| expect(result.output).toContain("deep") | ||
| expect(result.output).toContain("Deeply wrapped") | ||
| // The outer wrapper is default, so it should show as optional | ||
| expect(result.output).toContain("optional") | ||
| }, | ||
| }) | ||
| }) | ||
| }) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use key existence check in
mockDispatcher, not truthiness.if (responses[method])fails for valid mocked values that are falsy. Check property existence to avoid false “No mock” failures.🐛 Proposed fix
function mockDispatcher(responses: Record<string, any>) { dispatcherSpy?.mockRestore() dispatcherSpy = spyOn(Dispatcher, "call").mockImplementation(async (method: string) => { - if (responses[method]) return responses[method] + if (Object.prototype.hasOwnProperty.call(responses, method)) return responses[method] throw new Error(`No mock for ${method}`) }) }📝 Committable suggestion
🤖 Prompt for AI Agents