From 4afd140b61b6ab08be23bdd3105afd1c26adab54 Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Fri, 3 Jul 2026 15:05:17 +0530 Subject: [PATCH 1/2] Add BSE India adapter --- cli-manifest.json | 141 ++++++++++++++++++++++++++++ clis/bse-india/announcements.js | 40 ++++++++ clis/bse-india/bse-india.test.js | 154 +++++++++++++++++++++++++++++++ clis/bse-india/indices.js | 34 +++++++ clis/bse-india/movers.js | 43 +++++++++ clis/bse-india/quote.js | 48 ++++++++++ clis/bse-india/utils.js | 62 +++++++++++++ 7 files changed, 522 insertions(+) create mode 100644 clis/bse-india/announcements.js create mode 100644 clis/bse-india/bse-india.test.js create mode 100644 clis/bse-india/indices.js create mode 100644 clis/bse-india/movers.js create mode 100644 clis/bse-india/quote.js create mode 100644 clis/bse-india/utils.js diff --git a/cli-manifest.json b/cli-manifest.json index bfdb71e..be278e7 100644 --- a/cli-manifest.json +++ b/cli-manifest.json @@ -6003,6 +6003,147 @@ "modulePath": "brave/search.js", "sourceFile": "brave/search.js" }, + { + "site": "bse-india", + "name": "announcements", + "description": "Latest BSE corporate announcements, optionally filtered by text", + "access": "read", + "domain": "api.bseindia.com", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "query", + "type": "string", + "default": "", + "required": false, + "help": "Optional company/text filter" + }, + { + "name": "limit", + "type": "int", + "default": 10, + "required": false, + "help": "Max announcements (1-50)" + } + ], + "columns": [ + "rank", + "title", + "newsId", + "url" + ], + "type": "js", + "modulePath": "bse-india/announcements.js", + "sourceFile": "bse-india/announcements.js" + }, + { + "site": "bse-india", + "name": "indices", + "description": "BSE index snapshot, including Sensex and sector indices", + "access": "read", + "domain": "api.bseindia.com", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "limit", + "type": "int", + "default": 20, + "required": false, + "help": "Max indices (1-100)" + } + ], + "columns": [ + "rank", + "code", + "name", + "alias", + "price", + "change", + "changePct", + "updateTime", + "url" + ], + "type": "js", + "modulePath": "bse-india/indices.js", + "sourceFile": "bse-india/indices.js" + }, + { + "site": "bse-india", + "name": "movers", + "description": "BSE gainers, losers, or top turnover securities", + "access": "read", + "domain": "api.bseindia.com", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "type", + "type": "string", + "default": "gainers", + "required": false, + "help": "gainers / losers / turnover" + }, + { + "name": "limit", + "type": "int", + "default": 10, + "required": false, + "help": "Max rows (1-10)" + } + ], + "columns": [ + "rank", + "code", + "symbol", + "name", + "price", + "change", + "changePct", + "turnoverCr", + "volume", + "url" + ], + "type": "js", + "modulePath": "bse-india/movers.js", + "sourceFile": "bse-india/movers.js" + }, + { + "site": "bse-india", + "name": "quote", + "description": "BSE stock quote by symbol, company name, ISIN, or code", + "access": "read", + "domain": "api.bseindia.com", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "symbol", + "type": "str", + "required": true, + "positional": true, + "help": "BSE symbol, code, company name, or ISIN" + } + ], + "columns": [ + "code", + "symbol", + "name", + "isin", + "price", + "change", + "changePct", + "open", + "high", + "low", + "updateTime", + "url" + ], + "type": "js", + "modulePath": "bse-india/quote.js", + "sourceFile": "bse-india/quote.js" + }, { "site": "chaoxing", "name": "assignments", diff --git a/clis/bse-india/announcements.js b/clis/bse-india/announcements.js new file mode 100644 index 0000000..c84427d --- /dev/null +++ b/clis/bse-india/announcements.js @@ -0,0 +1,40 @@ +import { EmptyResultError } from '@agentrhq/webcmd/errors'; +import { cli, Strategy } from '@agentrhq/webcmd/registry'; +import { BSE_API, BSE_SITE, bseJson, requireLimit, text } from './utils.js'; + +function newsId(value) { + return String(value ?? '').split('&')[0].trim(); +} + +cli({ + site: 'bse-india', + name: 'announcements', + access: 'read', + description: 'Latest BSE corporate announcements, optionally filtered by text', + domain: 'api.bseindia.com', + strategy: Strategy.PUBLIC, + browser: false, + args: [ + { name: 'query', type: 'string', default: '', help: 'Optional company/text filter' }, + { name: 'limit', type: 'int', default: 10, help: 'Max announcements (1-50)' }, + ], + columns: ['rank', 'title', 'newsId', 'url'], + func: async (args) => { + const query = String(args.query ?? '').trim().toLowerCase(); + const limit = requireLimit(args.limit, 10, 50); + const body = await bseJson(`${BSE_API}/CorpAnn/w`, 'bse-india announcements'); + const rows = (Array.isArray(body) ? body : []) + .filter((row) => !query || String(row?.Subject ?? '').toLowerCase().includes(query)) + .slice(0, limit); + if (!rows.length) throw new EmptyResultError('bse-india announcements', query ? `No announcements matched "${args.query}".` : 'BSE returned no announcements.'); + return rows.map((row, i) => { + const id = newsId(row?.Newsid); + return { + rank: i + 1, + title: text(row?.Subject), + newsId: id, + url: id ? `${BSE_SITE}/corporates/anndet_new.aspx?newsid=${encodeURIComponent(id)}` : null, + }; + }); + }, +}); diff --git a/clis/bse-india/bse-india.test.js b/clis/bse-india/bse-india.test.js new file mode 100644 index 0000000..39f5683 --- /dev/null +++ b/clis/bse-india/bse-india.test.js @@ -0,0 +1,154 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { ArgumentError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors'; +import { getRegistry } from '@agentrhq/webcmd/registry'; +import './announcements.js'; +import './indices.js'; +import './movers.js'; +import './quote.js'; + +afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); +}); + +function json(body, status = 200) { + return new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' }, + }); +} + +describe('bse-india indices', () => { + const cmd = getRegistry().get('bse-india/indices'); + + it('maps index rows', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(json({ Table: [{ + indexName: 'BSE SENSEX', + LTP: 77782.46, + change: 280.34, + PERCENTCHG: 0.36, + DT_TM: '2026-07-03T15:00:46', + code: 16, + shortalias: 'SENSEX', + }] }))); + + await expect(cmd.func({ limit: 1 })).resolves.toEqual([{ + rank: 1, + code: '16', + name: 'BSE SENSEX', + alias: 'SENSEX', + price: 77782.46, + change: 280.34, + changePct: 0.36, + updateTime: '2026-07-03T15:00:46', + url: 'https://www.bseindia.com/sensex/code/16', + }]); + }); + + it('rejects invalid limits', async () => { + vi.stubGlobal('fetch', vi.fn()); + await expect(cmd.func({ limit: 0 })).rejects.toBeInstanceOf(ArgumentError); + await expect(cmd.func({ limit: 101 })).rejects.toBeInstanceOf(ArgumentError); + expect(fetch).not.toHaveBeenCalled(); + }); +}); + +describe('bse-india movers', () => { + const cmd = getRegistry().get('bse-india/movers'); + + it('maps gainers', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(json({ Table: [{ + ScripName: 'HCLTECH', + LONGNAME: 'HCL Technologies Ltd', + Ltradert: 1141.85, + change_val: 64.35, + change_percent: 5.97, + Trd_val: 120.5, + Trd_vol: 25000, + scrip_id: 'HCLTECH', + scrip_cd: 532281, + NSUrl: 'https://www.bseindia.com/stock-share-price/hcl-technologies-ltd/hcltech/532281/', + }] }))); + + await expect(cmd.func({ type: 'gainers', limit: 1 })).resolves.toEqual([{ + rank: 1, + code: '532281', + symbol: 'HCLTECH', + name: 'HCL Technologies Ltd', + price: 1141.85, + change: 64.35, + changePct: 5.97, + turnoverCr: 120.5, + volume: 25000, + url: 'https://www.bseindia.com/stock-share-price/hcl-technologies-ltd/hcltech/532281/', + }]); + }); + + it('rejects unknown mover types', async () => { + await expect(cmd.func({ type: 'active' })).rejects.toBeInstanceOf(ArgumentError); + }); +}); + +describe('bse-india announcements', () => { + const cmd = getRegistry().get('bse-india/announcements'); + + it('maps corporate announcements and filters by query', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(json([ + { Subject: 'Reliance Industries Ltd - Board Meeting', Newsid: 'abc&flag=1' }, + { Subject: 'Other company update', Newsid: 'def&flag=1' }, + ]))); + + await expect(cmd.func({ query: 'reliance', limit: 5 })).resolves.toEqual([{ + rank: 1, + title: 'Reliance Industries Ltd - Board Meeting', + newsId: 'abc', + url: 'https://www.bseindia.com/corporates/anndet_new.aspx?newsid=abc', + }]); + }); + + it('throws EmptyResultError when filtering removes all announcements', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(json([{ Subject: 'Other', Newsid: 'def' }]))); + await expect(cmd.func({ query: 'missing', limit: 5 })).rejects.toBeInstanceOf(EmptyResultError); + }); +}); + +describe('bse-india quote', () => { + const cmd = getRegistry().get('bse-india/quote'); + + it('searches by symbol and maps quote details', async () => { + vi.stubGlobal('fetch', vi.fn() + .mockResolvedValueOnce(json([{ + strSricpCode: '500325', + shortName: 'RELIANCE', + scripName: 'Reliance Industries Ltd', + Isin: 'INE002A01018', + SEOUrl: 'https://www.bseindia.com/stock-share-price/reliance-industries-ltd/reliance/500325/', + Type: 'in Equity T+1', + }])) + .mockResolvedValueOnce(json({ + CurrRate: { LTP: '1304.55', Chg: '+0.75', PcChg: '+0.06' }, + Cmpname: { FullN: 'Reliance Industries Ltd', ShortN: 'RELIANCE', SeriesN: 'A' }, + Header: { PrevClose: '1303.80', Open: '1305.00', High: '1310.00', Low: '1298.00', Ason: '03 Jul 26 | 15:00' }, + }))); + + await expect(cmd.func({ symbol: 'RELIANCE' })).resolves.toEqual([{ + code: '500325', + symbol: 'RELIANCE', + name: 'Reliance Industries Ltd', + isin: 'INE002A01018', + price: 1304.55, + change: 0.75, + changePct: 0.06, + open: 1305, + high: 1310, + low: 1298, + updateTime: '03 Jul 26 | 15:00', + url: 'https://www.bseindia.com/stock-share-price/reliance-industries-ltd/reliance/500325/', + }]); + }); + + it('maps HTTP failures to CommandExecutionError', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(json({}, 500))); + await expect(cmd.func({ symbol: 'RELIANCE' })).rejects.toBeInstanceOf(CommandExecutionError); + }); +}); diff --git a/clis/bse-india/indices.js b/clis/bse-india/indices.js new file mode 100644 index 0000000..09f57ee --- /dev/null +++ b/clis/bse-india/indices.js @@ -0,0 +1,34 @@ +import { cli, Strategy } from '@agentrhq/webcmd/registry'; +import { BSE_API, BSE_SITE, bseJson, requireLimit, tableRows, text, toNumber } from './utils.js'; + +cli({ + site: 'bse-india', + name: 'indices', + access: 'read', + description: 'BSE index snapshot, including Sensex and sector indices', + domain: 'api.bseindia.com', + strategy: Strategy.PUBLIC, + browser: false, + args: [ + { name: 'limit', type: 'int', default: 20, help: 'Max indices (1-100)' }, + ], + columns: ['rank', 'code', 'name', 'alias', 'price', 'change', 'changePct', 'updateTime', 'url'], + func: async (args) => { + const limit = requireLimit(args.limit, 20, 100); + const body = await bseJson(`${BSE_API}/IndexMovers/w`, 'bse-india indices'); + return tableRows(body, 'bse-india indices').slice(0, limit).map((row, i) => { + const code = String(row?.code ?? ''); + return { + rank: i + 1, + code, + name: text(row?.indexName), + alias: text(row?.shortalias), + price: toNumber(row?.LTP), + change: toNumber(row?.change), + changePct: toNumber(row?.PERCENTCHG), + updateTime: text(row?.DT_TM), + url: code ? `${BSE_SITE}/sensex/code/${code}` : null, + }; + }); + }, +}); diff --git a/clis/bse-india/movers.js b/clis/bse-india/movers.js new file mode 100644 index 0000000..de5f355 --- /dev/null +++ b/clis/bse-india/movers.js @@ -0,0 +1,43 @@ +import { ArgumentError } from '@agentrhq/webcmd/errors'; +import { cli, Strategy } from '@agentrhq/webcmd/registry'; +import { BSE_API, bseJson, requireLimit, tableRows, text, toNumber } from './utils.js'; + +const TYPES = { + gainers: 'G', + losers: 'L', + turnover: 'T', +}; + +cli({ + site: 'bse-india', + name: 'movers', + access: 'read', + description: 'BSE gainers, losers, or top turnover securities', + domain: 'api.bseindia.com', + strategy: Strategy.PUBLIC, + browser: false, + args: [ + { name: 'type', type: 'string', default: 'gainers', help: 'gainers / losers / turnover' }, + { name: 'limit', type: 'int', default: 10, help: 'Max rows (1-10)' }, + ], + columns: ['rank', 'code', 'symbol', 'name', 'price', 'change', 'changePct', 'turnoverCr', 'volume', 'url'], + func: async (args) => { + const type = String(args.type ?? 'gainers').trim().toLowerCase(); + const flag = TYPES[type]; + if (!flag) throw new ArgumentError(`Unknown bse-india movers type "${args.type}". Valid: gainers, losers, turnover`); + const limit = requireLimit(args.limit, 10, 10); + const body = await bseJson(`${BSE_API}/HoTurnover/w?flag=${flag}`, 'bse-india movers'); + return tableRows(body, 'bse-india movers').slice(0, limit).map((row, i) => ({ + rank: i + 1, + code: String(row?.scrip_cd ?? row?.SCRIPCODE ?? ''), + symbol: text(row?.scrip_id ?? row?.ScripName), + name: text(row?.LONGNAME ?? row?.LONGNAME1 ?? row?.ScripName), + price: toNumber(row?.Ltradert), + change: toNumber(row?.change_val), + changePct: toNumber(row?.change_percent), + turnoverCr: toNumber(row?.Trd_val), + volume: toNumber(row?.Trd_vol), + url: text(row?.NSUrl), + })); + }, +}); diff --git a/clis/bse-india/quote.js b/clis/bse-india/quote.js new file mode 100644 index 0000000..e2e0649 --- /dev/null +++ b/clis/bse-india/quote.js @@ -0,0 +1,48 @@ +import { EmptyResultError } from '@agentrhq/webcmd/errors'; +import { cli, Strategy } from '@agentrhq/webcmd/registry'; +import { BSE_API, bseJson, requireText, text, toNumber } from './utils.js'; + +async function findEquity(symbol) { + const query = encodeURIComponent(symbol); + const rows = await bseJson(`${BSE_API}/GetQuoteAllSearchDatabeta/w?searchString=${query}`, 'bse-india quote search'); + const matches = Array.isArray(rows) ? rows : []; + const exact = matches.find((row) => String(row?.shortName ?? '').toUpperCase() === symbol.toUpperCase() && /Equity/i.test(String(row?.Type ?? ''))); + const fallback = matches.find((row) => /Equity/i.test(String(row?.Type ?? ''))); + const match = exact ?? fallback; + if (!match) throw new EmptyResultError('bse-india quote', `No BSE equity matched "${symbol}".`); + return match; +} + +cli({ + site: 'bse-india', + name: 'quote', + access: 'read', + description: 'BSE stock quote by symbol, company name, ISIN, or code', + domain: 'api.bseindia.com', + strategy: Strategy.PUBLIC, + browser: false, + args: [ + { name: 'symbol', positional: true, required: true, help: 'BSE symbol, code, company name, or ISIN' }, + ], + columns: ['code', 'symbol', 'name', 'isin', 'price', 'change', 'changePct', 'open', 'high', 'low', 'updateTime', 'url'], + func: async (args) => { + const symbol = requireText(args.symbol, 'quote symbol'); + const match = await findEquity(symbol); + const code = String(match.strSricpCode ?? '').trim(); + const body = await bseJson(`${BSE_API}/getScripHeaderData/w?Debtflag=&scripcode=${encodeURIComponent(code)}&seriesid=`, 'bse-india quote'); + return [{ + code, + symbol: text(match.shortName ?? body?.Cmpname?.ShortN), + name: text(body?.Cmpname?.FullN ?? match.scripName), + isin: text(match.Isin), + price: toNumber(body?.CurrRate?.LTP ?? body?.Header?.LTP), + change: toNumber(body?.CurrRate?.Chg), + changePct: toNumber(body?.CurrRate?.PcChg), + open: toNumber(body?.Header?.Open), + high: toNumber(body?.Header?.High), + low: toNumber(body?.Header?.Low), + updateTime: text(body?.Header?.Ason), + url: text(match.SEOUrl), + }]; + }, +}); diff --git a/clis/bse-india/utils.js b/clis/bse-india/utils.js new file mode 100644 index 0000000..2922268 --- /dev/null +++ b/clis/bse-india/utils.js @@ -0,0 +1,62 @@ +import { ArgumentError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors'; + +export const BSE_API = 'https://api.bseindia.com/BseIndiaAPI/api'; +export const BSE_REALTIME_API = 'https://api.bseindia.com/RealTimeBseIndiaAPI/api'; +export const BSE_SITE = 'https://www.bseindia.com'; + +const HEADERS = { + accept: 'application/json', + referer: `${BSE_SITE}/`, + 'user-agent': 'Mozilla/5.0', +}; + +export function requireLimit(value, defaultValue = 10, maxValue = 100) { + const rawLimit = value ?? defaultValue; + const limit = Number(rawLimit); + if (!Number.isInteger(limit) || limit <= 0) { + throw new ArgumentError('bse-india limit must be a positive integer'); + } + if (limit > maxValue) { + throw new ArgumentError(`bse-india limit must be <= ${maxValue}`); + } + return limit; +} + +export function requireText(value, label) { + const text = String(value ?? '').trim(); + if (!text) throw new ArgumentError(`bse-india ${label} is required`); + return text; +} + +export function toNumber(value) { + if (value == null || value === '') return null; + const n = Number(String(value).replace(/[,+%]/g, '').trim()); + return Number.isFinite(n) ? n : null; +} + +export function text(value) { + if (value == null) return null; + const s = String(value).trim(); + return s || null; +} + +export async function bseJson(url, label) { + let resp; + try { + resp = await fetch(url, { headers: HEADERS }); + } catch (err) { + throw new CommandExecutionError(`${label} request failed: ${err?.message ?? err}`); + } + if (!resp.ok) throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`); + try { + return await resp.json(); + } catch (err) { + throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`); + } +} + +export function tableRows(body, label) { + const rows = Array.isArray(body?.Table) ? body.Table : []; + if (!rows.length) throw new EmptyResultError(label, 'BSE returned no rows.'); + return rows; +} From 3bad54de68ccb2a5f29032e90e1442494768e84d Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Sat, 4 Jul 2026 12:48:21 +0530 Subject: [PATCH 2/2] Fix BSE announcement query lookup --- clis/bse-india/announcements.js | 36 ++++++++++++++++++++++++++---- clis/bse-india/bse-india.test.js | 38 +++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/clis/bse-india/announcements.js b/clis/bse-india/announcements.js index c84427d..8942b2f 100644 --- a/clis/bse-india/announcements.js +++ b/clis/bse-india/announcements.js @@ -6,6 +6,33 @@ function newsId(value) { return String(value ?? '').split('&')[0].trim(); } +function announcementRows(body) { + let value = body; + if (typeof body === 'string') { + try { + value = JSON.parse(body); + } catch { + value = []; + } + } + return Array.isArray(value) ? value : []; +} + +function announcementTitle(row) { + return text(row?.Subject ?? row?.NewsSubj); +} + +async function companyNews(query) { + const matches = await bseJson(`${BSE_API}/GetQuoteAllSearchDatabeta/w?searchString=${encodeURIComponent(query)}`, 'bse-india announcements company search'); + const rows = Array.isArray(matches) ? matches : []; + const match = rows.find((row) => /Equity/i.test(String(row?.Type ?? ''))); + const code = String(match?.strSricpCode ?? '').trim(); + if (!code) + return null; + const body = await bseJson(`${BSE_API}/TabResults_PAR/w?scripcode=${encodeURIComponent(code)}&tabtype=NEWS`, 'bse-india announcements company news'); + return announcementRows(body); +} + cli({ site: 'bse-india', name: 'announcements', @@ -22,16 +49,17 @@ cli({ func: async (args) => { const query = String(args.query ?? '').trim().toLowerCase(); const limit = requireLimit(args.limit, 10, 50); - const body = await bseJson(`${BSE_API}/CorpAnn/w`, 'bse-india announcements'); - const rows = (Array.isArray(body) ? body : []) - .filter((row) => !query || String(row?.Subject ?? '').toLowerCase().includes(query)) + const companyRows = query ? await companyNews(query) : null; + const body = companyRows ? companyRows : announcementRows(await bseJson(`${BSE_API}/CorpAnn/w`, 'bse-india announcements')); + const rows = body + .filter((row) => companyRows || !query || String(row?.Subject ?? '').toLowerCase().includes(query)) .slice(0, limit); if (!rows.length) throw new EmptyResultError('bse-india announcements', query ? `No announcements matched "${args.query}".` : 'BSE returned no announcements.'); return rows.map((row, i) => { const id = newsId(row?.Newsid); return { rank: i + 1, - title: text(row?.Subject), + title: announcementTitle(row), newsId: id, url: id ? `${BSE_SITE}/corporates/anndet_new.aspx?newsid=${encodeURIComponent(id)}` : null, }; diff --git a/clis/bse-india/bse-india.test.js b/clis/bse-india/bse-india.test.js index 39f5683..bf49ba0 100644 --- a/clis/bse-india/bse-india.test.js +++ b/clis/bse-india/bse-india.test.js @@ -92,13 +92,13 @@ describe('bse-india movers', () => { describe('bse-india announcements', () => { const cmd = getRegistry().get('bse-india/announcements'); - it('maps corporate announcements and filters by query', async () => { + it('maps latest corporate announcements', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue(json([ { Subject: 'Reliance Industries Ltd - Board Meeting', Newsid: 'abc&flag=1' }, { Subject: 'Other company update', Newsid: 'def&flag=1' }, ]))); - await expect(cmd.func({ query: 'reliance', limit: 5 })).resolves.toEqual([{ + await expect(cmd.func({ limit: 1 })).resolves.toEqual([{ rank: 1, title: 'Reliance Industries Ltd - Board Meeting', newsId: 'abc', @@ -106,8 +106,40 @@ describe('bse-india announcements', () => { }]); }); + it('uses company search for announcement queries', async () => { + vi.stubGlobal('fetch', vi.fn() + .mockResolvedValueOnce(json([{ + strSricpCode: '500325', + shortName: 'RELIANCE', + scripName: 'Reliance Industries Ltd', + Type: 'in Equity T+1', + }])) + .mockResolvedValueOnce(json(JSON.stringify([ + { NewsSubj: 'Announcement under Regulation 30 (LODR)-Credit Rating', Newsid: 'abc&flag=1' }, + ])))); + + await expect(cmd.func({ query: 'reliance', limit: 5 })).resolves.toEqual([{ + rank: 1, + title: 'Announcement under Regulation 30 (LODR)-Credit Rating', + newsId: 'abc', + url: 'https://www.bseindia.com/corporates/anndet_new.aspx?newsid=abc', + }]); + expect(fetch).toHaveBeenNthCalledWith( + 1, + 'https://api.bseindia.com/BseIndiaAPI/api/GetQuoteAllSearchDatabeta/w?searchString=reliance', + expect.any(Object), + ); + expect(fetch).toHaveBeenNthCalledWith( + 2, + 'https://api.bseindia.com/BseIndiaAPI/api/TabResults_PAR/w?scripcode=500325&tabtype=NEWS', + expect.any(Object), + ); + }); + it('throws EmptyResultError when filtering removes all announcements', async () => { - vi.stubGlobal('fetch', vi.fn().mockResolvedValue(json([{ Subject: 'Other', Newsid: 'def' }]))); + vi.stubGlobal('fetch', vi.fn() + .mockResolvedValueOnce(json([])) + .mockResolvedValueOnce(json([{ Subject: 'Other', Newsid: 'def' }]))); await expect(cmd.func({ query: 'missing', limit: 5 })).rejects.toBeInstanceOf(EmptyResultError); }); });