feat(m15): full test completion + quality gate + validation fixes

- Add E2E notifications.spec (member-added notification -> list -> mark read)
  and activity-log.spec (board_posted + todo_created recorded)
- Set Playwright workers=1 for deterministic full-suite runs (SSE test under
  parallel load); full E2E suite (18 tests / 13 specs incl. 12 mandated
  scenarios) green
- Quality gate: 251 unit/integration tests, lint clean, typecheck clean,
  build succeeds, coverage 91.13% Repository/Service (>=80%)
- Validation fixes (mandatory): SearchService now searches ALL items
  (large pageSize) instead of first page only; MeetingService.
  checkScheduleConflicts now requires membership (actorId) to prevent
  non-member info enumeration
This commit is contained in:
Ken Yasue
2026-06-25 02:54:48 +02:00
parent 2bc883cb6f
commit f9720850b2
7 changed files with 145 additions and 5 deletions

View File

@ -31,6 +31,9 @@ export interface SearchOptions {
type?: SearchResourceType;
}
// 検索は全件対象とする(ページネーション既定値による検索漏れを防ぐ)
const SEARCH_MAX = Number.MAX_SAFE_INTEGER;
/**
* プロジェクト内の横断検索を担うService。
* 各リソースを取得しキーワードで絞り込む(小規模データ前提)。
@ -62,7 +65,10 @@ export class SearchService {
!!text && text.toLowerCase().includes(q);
if (!type || type === 'thread') {
for (const t of this.boardRepository.findThreads(projectId).items) {
for (const t of this.boardRepository.findThreads(projectId, {
page: 1,
pageSize: SEARCH_MAX,
}).items) {
if (match(t.title) || match(t.bodyMd)) {
results.push({
type: 'thread',
@ -74,7 +80,10 @@ export class SearchService {
}
}
if (!type || type === 'chat') {
for (const m of this.chatRepository.findMessages(projectId).items) {
for (const m of this.chatRepository.findMessages(projectId, {
page: 1,
pageSize: SEARCH_MAX,
}).items) {
if (match(m.body)) {
results.push({
type: 'chat',
@ -101,7 +110,7 @@ export class SearchService {
const files = this.fileRepository.findFilesByProject(
projectId,
1,
500
SEARCH_MAX
).items;
for (const f of files) {
if (match(f.originalName)) {
@ -156,7 +165,10 @@ export class SearchService {
}
}
if (!type || type === 'note') {
for (const n of this.noteRepository.findNotes(projectId).items) {
for (const n of this.noteRepository.findNotes(projectId, {
page: 1,
pageSize: SEARCH_MAX,
}).items) {
if (match(n.title) || match(n.bodyMd) || match(n.tags)) {
results.push({
type: 'note',