From 0df47febf0a5d05d852dcc63985d51bc7ad5b4b8 Mon Sep 17 00:00:00 2001 From: altair823 Date: Sat, 30 May 2026 02:24:24 +0000 Subject: [PATCH] =?UTF-8?q?test(store):=20doc-side=20expansion=20Task=202?= =?UTF-8?q?=20=EB=A6=AC=EB=B7=B0=20=EB=B3=B4=EA=B0=95=20(M1/M2/N1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M1: chunk_aliases trigger 가드에 AND aliases <> '' (빈 문자열 미색인) - M2: 재색인 멱등 테스트 (재-put 후 별칭 행 1개) - N1: 본문 격리 음성 단언 (별칭 term 이 chunks_fts 로 누출 안 됨) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../kebab-store-sqlite/tests/chunk_aliases.rs | 57 +++++++++++++++++++ migrations/V010__chunk_aliases.sql | 8 ++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/crates/kebab-store-sqlite/tests/chunk_aliases.rs b/crates/kebab-store-sqlite/tests/chunk_aliases.rs index 96c7cd4..cbfc818 100644 --- a/crates/kebab-store-sqlite/tests/chunk_aliases.rs +++ b/crates/kebab-store-sqlite/tests/chunk_aliases.rs @@ -161,3 +161,60 @@ fn none_aliases_not_indexed() { "aliases=None 이면 chunk_aliases_fts 에 행이 없어야 한다" ); } + +/// Task 2 리뷰 M2: 같은 doc 을 두 번 `put_chunks` 해도 `chunk_aliases_fts` +/// 행이 중복되지 않아야 한다. put_chunks 의 DELETE-then-INSERT 가 +/// chunk_aliases_ad → chunk_aliases_ai 를 발화해 멱등 재동기화하는지 검증. +#[test] +fn reput_keeps_single_alias_row() { + let env = common::TestEnv::new(); + let store = open_store_with_document(&env); + let doc = DocumentId("d".repeat(32)); + let mk = || base_chunk(&"e".repeat(32), &doc, Some("메모리 안전성".into())); + + store.put_chunks(&doc, &[mk()]).unwrap(); + store.put_chunks(&doc, &[mk()]).unwrap(); // 같은 doc 재-put + + let n: i64 = env.with_conn(|c| { + c.query_row("SELECT count(*) FROM chunk_aliases_fts", [], |r| r.get(0)) + }); + assert_eq!(n, 1, "재색인 후에도 별칭 행은 1개여야 한다 (중복/누락 없음)"); +} + +/// Task 2 리뷰 N1: 별칭 term 이 본문 `chunks_fts` 로 새지 않아야 한다(§3.3 격리). +/// 본문엔 없고 별칭에만 있는 한국어 term 으로 chunks_fts 를 MATCH 하면 0행. +#[test] +fn aliases_dont_leak_into_body_fts() { + let env = common::TestEnv::new(); + let store = open_store_with_document(&env); + let doc = DocumentId("d".repeat(32)); + // 본문 "Rust ownership and borrowing" 에 "메모리" 없음, 별칭에만 있음. + let chunk = base_chunk(&"e".repeat(32), &doc, Some("메모리 안전성".into())); + store.put_chunks(&doc, &[chunk]).unwrap(); + + let body_hits: i64 = env.with_conn(|c| { + c.query_row( + "SELECT count(*) FROM chunks_fts WHERE chunks_fts MATCH 'text : (\"메모리\")'", + [], + |r| r.get(0), + ) + }); + assert_eq!(body_hits, 0, "별칭 term 이 본문 chunks_fts 로 누출되면 안 된다"); +} + +/// Task 2 리뷰 M1: 빈 문자열 별칭은 색인하지 않는다(trigger 가드 +/// `AND new.aliases <> ''`). producer 가 Some("") 를 넘겨도 무용한 행이 +/// chunk_aliases_fts 에 쌓이지 않아야 한다. +#[test] +fn empty_string_alias_not_indexed() { + let env = common::TestEnv::new(); + let store = open_store_with_document(&env); + let doc = DocumentId("d".repeat(32)); + let chunk = base_chunk(&"e".repeat(32), &doc, Some(String::new())); + store.put_chunks(&doc, &[chunk]).unwrap(); + + let n: i64 = env.with_conn(|c| { + c.query_row("SELECT count(*) FROM chunk_aliases_fts", [], |r| r.get(0)) + }); + assert_eq!(n, 0, "빈 문자열 별칭은 chunk_aliases_fts 에 색인되면 안 된다"); +} diff --git a/migrations/V010__chunk_aliases.sql b/migrations/V010__chunk_aliases.sql index 130dbad..e88d19e 100644 --- a/migrations/V010__chunk_aliases.sql +++ b/migrations/V010__chunk_aliases.sql @@ -16,7 +16,10 @@ CREATE VIRTUAL TABLE chunk_aliases_fts USING fts5( tokenize = 'unicode61' ); -CREATE TRIGGER chunk_aliases_ai AFTER INSERT ON chunks WHEN new.aliases IS NOT NULL BEGIN +-- 가드 `IS NOT NULL AND <> ''`: producer 가 Some("") 를 넘겨도 내용 없는 +-- 행이 chunk_aliases_fts 에 쌓이지 않게 한다(Task 2 리뷰 M1). +CREATE TRIGGER chunk_aliases_ai AFTER INSERT ON chunks + WHEN new.aliases IS NOT NULL AND new.aliases <> '' BEGIN INSERT INTO chunk_aliases_fts(chunk_id, doc_id, aliases) VALUES (new.chunk_id, new.doc_id, new.aliases); END; @@ -26,7 +29,8 @@ END; CREATE TRIGGER chunk_aliases_au AFTER UPDATE ON chunks BEGIN DELETE FROM chunk_aliases_fts WHERE chunk_id = old.chunk_id; INSERT INTO chunk_aliases_fts(chunk_id, doc_id, aliases) - SELECT new.chunk_id, new.doc_id, new.aliases WHERE new.aliases IS NOT NULL; + SELECT new.chunk_id, new.doc_id, new.aliases + WHERE new.aliases IS NOT NULL AND new.aliases <> ''; END; -- in-process LRU search cache 무효화 (V009 와 동일 패턴).