v2.2.320: /naver-shopping "CATEGORY_TREE is not iterable" 크래시 수정 + 카테고리 목록 조회
- import * as (namespace import) 로 배열 JSON 을 불러오면 esbuild 번들에서
{ default: [...] } 로 감싸져 배열이 아니게 되는 문제 — import x = require(...)
로 전환해 esbuild/tsc/ts-jest 세 경로 모두 동일하게 동작하도록 수정
- 실제 esbuild 번들 산출물을 실행하는 회귀 테스트 추가 (ts-jest만으론 이 버그를 못 잡음)
- /naver-shopping categories [검색어] — 대분류 목록/세부 카테고리 검색 명령 추가
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "직답 결과 — single-pass mock 응답입니다.",
|
||||
"createdAt": 1785921598765,
|
||||
"createdAt": 1785978343885,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "---\nid: wiki_on\ndate: 2026-08-05T09:19:58.767Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\n직답 결과 — single-pass mock 응답입니다.\n\n직답 결과 — single-pass mock 응답입니다.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `0/100` | ✅ Low |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[DIRECT]** 답변 작성 중... (단일 호출 fast-path) (22ms)\n",
|
||||
"createdAt": 1785921598767,
|
||||
"result": "---\nid: wiki_on\ndate: 2026-08-06T01:05:43.891Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\n직답 결과 — single-pass mock 응답입니다.\n\n직답 결과 — single-pass mock 응답입니다.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `0/100` | ✅ Low |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[DIRECT]** 답변 작성 중... (단일 호출 fast-path) (17ms)\n",
|
||||
"createdAt": 1785978343891,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"missionId": "wiki_on",
|
||||
"status": "completed",
|
||||
"startTime": "2026-08-05T09:19:58.741Z",
|
||||
"startTime": "2026-08-06T01:05:43.866Z",
|
||||
"totalElapsedMs": 27,
|
||||
"results": {
|
||||
"direct": "직답 결과 — single-pass mock 응답입니다."
|
||||
@@ -12,16 +12,16 @@
|
||||
{
|
||||
"from": "idle",
|
||||
"to": "direct",
|
||||
"durationMs": 22,
|
||||
"durationMs": 17,
|
||||
"message": "답변 작성 중... (단일 호출 fast-path)",
|
||||
"ts": "2026-08-05T09:19:58.763Z"
|
||||
"ts": "2026-08-06T01:05:43.883Z"
|
||||
},
|
||||
{
|
||||
"from": "direct",
|
||||
"to": "completed",
|
||||
"durationMs": 5,
|
||||
"durationMs": 9,
|
||||
"message": "미션 완료",
|
||||
"ts": "2026-08-05T09:19:58.768Z"
|
||||
"ts": "2026-08-06T01:05:43.892Z"
|
||||
}
|
||||
],
|
||||
"resilienceMetrics": {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
||||
"createdAt": 1785921605167,
|
||||
"createdAt": 1785978351712,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
||||
"createdAt": 1785921605166,
|
||||
"createdAt": 1785978351711,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]",
|
||||
"createdAt": 1785921605162,
|
||||
"createdAt": 1785978351707,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||
"createdAt": 1785921605164,
|
||||
"createdAt": 1785978351709,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+10
-10
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"missionId": "stress_conflict_1785921605143",
|
||||
"missionId": "stress_conflict_1785978351690",
|
||||
"status": "completed",
|
||||
"startTime": "2026-08-05T09:20:05.143Z",
|
||||
"totalElapsedMs": 25,
|
||||
"startTime": "2026-08-06T01:05:51.690Z",
|
||||
"totalElapsedMs": 23,
|
||||
"results": {
|
||||
"outline": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]",
|
||||
"section_0": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||
@@ -14,30 +14,30 @@
|
||||
{
|
||||
"from": "idle",
|
||||
"to": "outline",
|
||||
"durationMs": 18,
|
||||
"durationMs": 16,
|
||||
"message": "답변 구조 잡는 중...",
|
||||
"ts": "2026-08-05T09:20:05.161Z"
|
||||
"ts": "2026-08-06T01:05:51.706Z"
|
||||
},
|
||||
{
|
||||
"from": "outline",
|
||||
"to": "section",
|
||||
"durationMs": 2,
|
||||
"durationMs": 3,
|
||||
"message": "본문 작성 중...",
|
||||
"ts": "2026-08-05T09:20:05.163Z"
|
||||
"ts": "2026-08-06T01:05:51.709Z"
|
||||
},
|
||||
{
|
||||
"from": "section",
|
||||
"to": "polish",
|
||||
"durationMs": 2,
|
||||
"durationMs": 1,
|
||||
"message": "최종 다듬기 중...",
|
||||
"ts": "2026-08-05T09:20:05.165Z"
|
||||
"ts": "2026-08-06T01:05:51.710Z"
|
||||
},
|
||||
{
|
||||
"from": "polish",
|
||||
"to": "completed",
|
||||
"durationMs": 3,
|
||||
"message": "미션 완료",
|
||||
"ts": "2026-08-05T09:20:05.168Z"
|
||||
"ts": "2026-08-06T01:05:51.713Z"
|
||||
}
|
||||
],
|
||||
"resilienceMetrics": {
|
||||
@@ -1,5 +1,13 @@
|
||||
# Astra Patch Notes
|
||||
|
||||
## v2.2.320 (2026-08-05)
|
||||
### 🐛 `/naver-shopping` "CATEGORY_TREE is not iterable" 크래시 수정 + 카테고리 목록 조회
|
||||
실사례: 배포 직후 `/naver-shopping 요즘 뜨는 뷰티 카테고리 알려줘` 실행 시 `CATEGORY_TREE is not iterable` 에러. 원인: `import * as x from './data.json'`(네임스페이스 import)를 esbuild 로 번들하면 배열 최상위 JSON 이 `{ default: [...] }` 로 감싸져 `x` 자체가 배열이 아니게 됨 — ts-jest 는 require() 그대로라 이 클래스의 버그를 테스트로 못 잡는다(실제로 기존 유닛 테스트는 전부 통과했었음).
|
||||
- **수정**: `import x = require(...)`(TS 의 순수 CJS require 문법)로 전환 — esbuild·tsc·ts-jest 세 경로 모두 동일하게 파싱된 배열을 그대로 받는다. (`esModuleInterop: true` 로도 고칠 수 있었지만, 이 프로젝트에서는 그 옵션이 `jest.spyOn(fs, 'statSync')` 같은 기존 CJS 네임스페이스 모킹을 깨서(Cannot redefine property) 되돌림.)
|
||||
- **회귀 테스트 추가**: `tests/naverTrend.test.ts` 에 esbuild 로 실제 번들링한 산출물을 실행해 검증하는 테스트 추가 — ts-jest 만으로는 못 잡는 이 클래스의 버그를 앞으로 CI 가 잡는다.
|
||||
- **카테고리 목록 조회 추가**: `/naver-shopping categories` (대분류 11종), `/naver-shopping categories <검색어>` (트리 fuzzy 검색 — 이름·경로·cat_id 표) — 어떤 분야명을 물어야 할지 몰라서 못 쓰는 문제 해결.
|
||||
검증: tsc 무오류 + jest 1013 통과(신규 1) + esbuild 정상.
|
||||
|
||||
## v2.2.319 (2026-08-05)
|
||||
### 🛍️ 네이버 쇼핑인사이트 · 검색어트렌드 연동 — `/naver-shopping`, `/naver-trend`
|
||||
사용자 요청: 네이버 쇼핑인사이트·검색어트렌드 API로 "오늘 검색어 많은 키워드 10개" 같은 자연어 질문에 답하고 싶음. 두 API 모두 비교 대상(분야 코드/키워드 묶음)을 미리 지정해야 상대 비율(ratio)만 돌려주는 구조라 "전체 실시간 top-N"을 직접 못 준다 — LLM이 도메인 지식으로 후보를 제안하고 실제 API로 검증해 순위를 매기는 방식(discover 모드)으로 갭을 메웠다.
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "astra",
|
||||
"displayName": "Astra",
|
||||
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
|
||||
"version": "2.2.319",
|
||||
"version": "2.2.320",
|
||||
"publisher": "g1nation",
|
||||
"license": "MIT",
|
||||
"icon": "assets/icon.png",
|
||||
|
||||
@@ -13,7 +13,15 @@
|
||||
* isnow890/naver-search-mcp, MIT) 에서 fuzzy 매칭으로 찾는다.
|
||||
*/
|
||||
|
||||
import * as categoryTreeRaw from './data/naverShoppingCategories.json';
|
||||
// ES 네임스페이스 import(`import * as x`)는 esbuild 의 JSON 로더에서 배열 최상위
|
||||
// JSON 을 `{ default: [...] }` 로 감싸버려 `x` 자체는 배열이 아니게 된다(ts-jest 는
|
||||
// require() 그대로라 이 문제가 안 보여 테스트를 통과하고도 실제 번들에서만
|
||||
// "CATEGORY_TREE is not iterable" 로 터졌다). ES default import 는 esModuleInterop
|
||||
// 이 필요한데, 이 프로젝트는 그 옵션을 켜면 `jest.spyOn(fs, 'statSync')` 같은 기존
|
||||
// 테스트의 CJS 네임스페이스 모킹이 깨진다(Cannot redefine property). 그래서 여기선
|
||||
// TS 의 `import x = require(...)` — 순수 CJS require 그대로 — 를 써서 esbuild·tsc·
|
||||
// ts-jest 세 경로 모두에서 동일하게 배열 그대로를 받는다.
|
||||
import categoryTreeRaw = require('./data/naverShoppingCategories.json');
|
||||
|
||||
export interface CategoryRow {
|
||||
code: string;
|
||||
|
||||
@@ -18,7 +18,7 @@ import * as vscode from 'vscode';
|
||||
import { registerSlashCommand, chunk } from '../datacollect/slashRouter';
|
||||
import { AIService } from '../../core/services';
|
||||
import { parseShoppingIntent, parseSearchTrendIntent } from './intentParser';
|
||||
import { findCategoryCode, listTopLevelCategoryNames, type CategoryMatch } from './categoryCodes';
|
||||
import { findCategoryCode, listTopLevelCategoryNames, ROOT_CATEGORY_CODES, type CategoryMatch } from './categoryCodes';
|
||||
import { rankCategories, rankKeywords, type RankedResult } from './rankEngine';
|
||||
import type { CommonFilters, TimeUnit } from './naverApi';
|
||||
|
||||
@@ -73,17 +73,47 @@ function cmdShoppingHelp(view: Webview): void {
|
||||
'',
|
||||
'- `/naver-shopping 패션의류 vs 화장품/미용 최근 일주일` — 명시 분야 비교',
|
||||
'- `/naver-shopping 요즘 뜨는 뷰티 카테고리 알려줘` — 탐색(후보 제안 + 검증)',
|
||||
'- `/naver-shopping categories` — 대분류(11종) 목록 보기',
|
||||
'- `/naver-shopping categories 원피스` — 카테고리 검색(이름 · 경로 · cat_id)',
|
||||
'',
|
||||
'- API 키 설정: Astra 설정 → Naver Trend 탭에서 Client ID/Secret 입력',
|
||||
'',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
/** `/naver-shopping categories [검색어]` — 검색어 없으면 대분류 11종, 있으면 트리 fuzzy 검색 결과. */
|
||||
function cmdListCategories(view: Webview, query: string): void {
|
||||
if (!query) {
|
||||
const lines = [
|
||||
'',
|
||||
'**네이버쇼핑 대분류 (11종)**',
|
||||
'',
|
||||
...listTopLevelCategoryNames().map(n => `- ${n}${ROOT_CATEGORY_CODES[n] ? ` (cat_id: ${ROOT_CATEGORY_CODES[n]})` : ''}`),
|
||||
'',
|
||||
'세부 카테고리 검색: `/naver-shopping categories <키워드>` (예: `/naver-shopping categories 원피스`)',
|
||||
'',
|
||||
];
|
||||
chunk(view, lines.join('\n'));
|
||||
return;
|
||||
}
|
||||
const matches = findCategoryCode(query, 15);
|
||||
if (matches.length === 0) {
|
||||
chunk(view, `\n❓ "${query}" 에 매칭되는 카테고리를 찾지 못했습니다.\n`);
|
||||
return;
|
||||
}
|
||||
const lines = ['', `**"${query}" 카테고리 검색 결과**`, '', '| 이름 | 경로 | cat_id |', '|---|---|---|'];
|
||||
for (const m of matches) lines.push(`| ${m.name} | ${m.path} | ${m.code} |`);
|
||||
chunk(view, lines.join('\n') + '\n');
|
||||
}
|
||||
|
||||
async function runShopping(arg: string, view: Webview, context?: vscode.ExtensionContext): Promise<boolean> {
|
||||
if (!context) { chunk(view, '\n⚠️ ExtensionContext 없음.\n'); return true; }
|
||||
const trimmed = arg.trim();
|
||||
if (!trimmed) { cmdShoppingHelp(view); return true; }
|
||||
|
||||
const listMatch = trimmed.match(/^(?:categories|category|카테고리|list)\b\s*(.*)$/i);
|
||||
if (listMatch) { cmdListCategories(view, listMatch[1].trim()); return true; }
|
||||
|
||||
try {
|
||||
chunk(view, '\n🔍 질문 분석 중…\n');
|
||||
const ai = new AIService();
|
||||
@@ -98,7 +128,7 @@ async function runShopping(arg: string, view: Webview, context?: vscode.Extensio
|
||||
}
|
||||
|
||||
if (resolved.length === 0) {
|
||||
chunk(view, `\n❓ 분야를 찾지 못했습니다: ${unresolved.join(', ')}\n\n지원 대분류 예시: ${listTopLevelCategoryNames().join(', ')}\n(세부 카테고리명으로도 검색 가능 — 예: "원피스", "무선이어폰")\n`);
|
||||
chunk(view, `\n❓ 분야를 찾지 못했습니다: ${unresolved.join(', ')}\n\n지원 대분류 예시: ${listTopLevelCategoryNames().join(', ')}\n(세부 카테고리명으로도 검색 가능 — 예: "원피스", "무선이어폰")\n전체 목록/검색: \`/naver-shopping categories [검색어]\`\n`);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
import { findCategoryCode, ROOT_CATEGORY_CODES } from '../src/features/naverTrend/categoryCodes';
|
||||
import { parseShoppingIntent, parseSearchTrendIntent } from '../src/features/naverTrend/intentParser';
|
||||
import type { IAIService, AIChatResult } from '../src/core/services';
|
||||
import * as esbuild from 'esbuild';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
function mockAi(content: string, shouldThrow = false): IAIService {
|
||||
return {
|
||||
@@ -107,3 +111,33 @@ describe('parseSearchTrendIntent — tolerant JSON 파싱', () => {
|
||||
expect(r.keywords).toEqual(['원피스 키워드']);
|
||||
});
|
||||
});
|
||||
|
||||
// v2.2.319 회귀: `import * as x from './data.json'`(namespace import) 를 esbuild 로
|
||||
// 번들하면 배열 최상위 JSON 이 `{ default: [...] }` 로 감싸져 x 자체가 배열이 아니게
|
||||
// 된다. ts-jest 는 require() 를 그대로 써서 이 버그가 안 보여 통과했지만 실제 배포
|
||||
// 번들(esbuild)에서 "CATEGORY_TREE is not iterable" 로 터졌다. default import 로
|
||||
// 고친 뒤에도 재발 안 하도록 실제 esbuild 번들 결과를 실행해 검증한다.
|
||||
describe('categoryCodes.ts — esbuild 번들 회귀 방지', () => {
|
||||
test('esbuild 로 번들링한 결과에서도 카테고리 트리가 정상 로드됨', () => {
|
||||
const entry = path.join(__dirname, '../src/features/naverTrend/categoryCodes.ts');
|
||||
const result = esbuild.buildSync({
|
||||
entryPoints: [entry],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
write: false,
|
||||
});
|
||||
const tmpFile = path.join(os.tmpdir(), `categoryCodes.bundle.${process.pid}.${Date.now()}.js`);
|
||||
fs.writeFileSync(tmpFile, result.outputFiles[0].text);
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const mod = require(tmpFile);
|
||||
const names = mod.listTopLevelCategoryNames();
|
||||
expect(Array.isArray(names)).toBe(true);
|
||||
expect(names.length).toBeGreaterThan(5);
|
||||
expect(mod.findCategoryCode('패션의류')[0]).toMatchObject({ code: '50000000', source: 'root' });
|
||||
} finally {
|
||||
fs.unlinkSync(tmpFile);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user