fa397d7fa1
[/email — Gmail + 클래식 Outlook (신규 기능군)]
- 기본 명령: brief(PM 업무 허브 대시보드)·list·read·wikify·reply(Drafts만)·open(Outlook 창 열기)
- Gmail: 자체 OAuth gmail.readonly 스코프 추가 (재연결 1회 필요) / Outlook: COM(PowerShell), 전 계정(Store)·하위 폴더(120개 가드) 수집, To/CC 판별
- 브리핑: 상태 3분류(결정/회신/확인만)·P등급 영향·프로젝트 위험도·Today Focus·오늘 완료 예상·기간 명시 시 표시 범위 제한
- 영속 상태(email_state.json): 회신대기/공넘김 상태머신, 창 밖 미회신 보존, done old N 일괄정리
- 워처: 주기 자동 수집 + 미회신 임계 알림(기본 끔) + 데일리 브리핑 '답할 이메일' 섹션
- 전체 스캔(/email scan): 자동 위키화(회차 15건·레지스트리·파일삭제 감지 재생성)·진행 표시(streamProgress)
- 자동발신 필터 확장(Confluence·AWS·광고·[Notification]) + 라벨 오타 후처리(polishBriefLabels)
[/stocks]
- discover 자동 편입(stocks.json) + 매수권장가 224일선 자동 산출
- 매수사정권 진입 전이 감지 → 텔레그램 1회 알림 (직전신호 추적)
- 버그 수정: '미충족'이 .includes('충족')에 오탐되던 필터 판정
[지식 문서 파이프라인 통일 (wikiFormat/wikiSave)]
- PC 로컬 직접 저장 (NAS 볼륨 미아 문제 해소) — 설정 폴더 → 두뇌 → 워크스페이스
- 이메일 전용 Email_Wiki 분리(email.wikifySavePath) + 봉투(frontmatter) 자동 통일
- 정본 프롬프트 단일 빌더(buildKnowledgeWikiPrompt) — web/email 래퍼화, 템플릿 fetch 일원화
- 이메일 문서 시점 3중 방어: 기준시점 헤더·제목=메일 최신일·날짜 없는 현재형 금지
- docs/WIKI_DOC_PIPELINE.md 개발 규약
[텔레그램 메신저 중앙화 (messageFormat)]
- 단일 렌더러 renderTelegram — 5개 발신 빌더(주식보고·매수시그널·Top5·데일리·미회신) 통일
- 주식 보고 시각 하드코딩 제거(messenger.stocksReportTimes CSV) + 알림 토글 2종
- 설정 패널 '메신저' 탭 신설 · '이메일' 토글 섹션(Gmail/Outlook on/off)
테스트 803개 통과 (이메일 상태머신·CC판별·포맷·매수전이·봉투 등 60+ 신규)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
75 lines
3.0 KiB
TypeScript
75 lines
3.0 KiB
TypeScript
/**
|
|
* v2.2.273 — 지식 문서 로컬 저장 (순수 로직: 파일명 안전화 + 저장 폴더 우선순위).
|
|
*/
|
|
import * as path from 'path';
|
|
import { sanitizeWikiFileName, pickWikiDir } from '../src/features/datacollect/wikiSave';
|
|
|
|
describe('sanitizeWikiFileName', () => {
|
|
test('금지 문자 치환 + .md 보장', () => {
|
|
const f = sanitizeWikiFileName('회의록 a/b:c*d?e<f>g|h 2026-07-03');
|
|
expect(f).not.toMatch(/[\\/:*?"<>|]/);
|
|
expect(f.endsWith('.md')).toBe(true);
|
|
expect(f).toContain('회의록');
|
|
});
|
|
|
|
test('빈 제목 → untitled, 120자 캡', () => {
|
|
expect(sanitizeWikiFileName('')).toMatch(/^untitled_\d+\.md$/);
|
|
expect(sanitizeWikiFileName('가'.repeat(300)).length).toBeLessThanOrEqual(124);
|
|
});
|
|
|
|
test('.md 중복 방지', () => {
|
|
expect(sanitizeWikiFileName('문서.md')).toBe('문서.md');
|
|
});
|
|
});
|
|
|
|
describe('pickWikiDir — 저장 폴더 우선순위', () => {
|
|
const abs = path.resolve('E:/커스텀/위키');
|
|
|
|
test('① 설정 절대경로 최우선', () => {
|
|
const r = pickWikiDir(abs, 'E:/brain', 'E:/ws');
|
|
expect(r).toEqual({ dir: abs, label: '설정 폴더' });
|
|
});
|
|
|
|
test('② 설정 없으면 두뇌/Datacollect_Wiki', () => {
|
|
const r = pickWikiDir('', 'E:/brain', 'E:/ws');
|
|
expect(r!.label).toBe('두뇌');
|
|
expect(r!.dir).toBe(path.join('E:/brain', 'Datacollect_Wiki'));
|
|
});
|
|
|
|
test('③ 두뇌 없으면 워크스페이스/.astra/wiki', () => {
|
|
const r = pickWikiDir('', '', 'E:/ws');
|
|
expect(r!.label).toBe('워크스페이스');
|
|
expect(r!.dir).toBe(path.join('E:/ws', '.astra', 'wiki'));
|
|
});
|
|
|
|
test('상대경로 설정은 무시하고 다음 후보로 (경로 오염 방지)', () => {
|
|
const r = pickWikiDir('상대/경로', 'E:/brain', null);
|
|
expect(r!.label).toBe('두뇌');
|
|
});
|
|
|
|
test('④ 전부 없으면 null', () => {
|
|
expect(pickWikiDir('', '', null)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('pickWikiDir — 이메일 지식 문서 별도 관리 (kind=email)', () => {
|
|
test('기본 폴더가 Email_Wiki 로 분리 — 일반(Datacollect_Wiki)과 다름', () => {
|
|
const email = pickWikiDir('', 'E:/brain', 'E:/ws', 'email');
|
|
const general = pickWikiDir('', 'E:/brain', 'E:/ws', 'general');
|
|
expect(email!.dir).toBe(path.join('E:/brain', 'Email_Wiki'));
|
|
expect(general!.dir).toBe(path.join('E:/brain', 'Datacollect_Wiki'));
|
|
expect(email!.dir).not.toBe(general!.dir);
|
|
});
|
|
|
|
test('이메일 전용 설정 경로가 최우선', () => {
|
|
const abs = path.resolve('E:/이메일위키');
|
|
const r = pickWikiDir(abs, 'E:/brain', 'E:/ws', 'email');
|
|
expect(r).toEqual({ dir: abs, label: '설정 폴더' });
|
|
});
|
|
|
|
test('두뇌 없으면 워크스페이스 .astra/wiki/email', () => {
|
|
const r = pickWikiDir('', '', 'E:/ws', 'email');
|
|
expect(r!.dir).toBe(path.join('E:/ws', '.astra', 'wiki', 'email'));
|
|
});
|
|
});
|