@gaonjs/cli 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,10 @@
22
22
  import { spawn } from 'node:child_process';
23
23
  import { existsSync, readFileSync } from 'node:fs';
24
24
  import { join } from 'node:path';
25
+ import { loadGaonConfig } from '@gaonjs/config';
26
+ import { deriveTestDatabaseConfig, ensureTestDatabaseExists, destroyAllConnections, } from '@gaonjs/data';
27
+ import { registerTsResolve } from '../tsResolve.js';
28
+ import { runDbMigrate } from '../db/migrate.js';
25
29
  /** 사용자 프로젝트에 `test` 스크립트가 있는지. */
26
30
  function hasTestScript(cwd) {
27
31
  const pkgPath = join(cwd, 'package.json');
@@ -52,6 +56,66 @@ function scopeArgs(scope) {
52
56
  }
53
57
  return [];
54
58
  }
59
+ /**
60
+ * 결정 111: 테스트 전용 DB 자동 준비 — gaon.config 의 각 커넥션을 `<db>_test` 로
61
+ * 파생해 없으면 만들고(CREATE DATABASE) 마이그레이션을 적용한다. 테스트 프로세스는
62
+ * test/setup.ts 의 `connectTestDatabase()` 로 같은 DB 에 붙고, 매 테스트 뒤
63
+ * `truncateAll()` 로 격리한다(§9 실 인프라 · truncate 격리 근거는 결정 111).
64
+ *
65
+ * scope='unit' 은 실 인프라가 필요 없으므로 건너뛴다. config 가 없거나 db 커넥션이
66
+ * 없으면 조용히 스킵(순수 vitest 위임). DB 접속 실패는 수리 안내와 함께 실패한다.
67
+ */
68
+ async function provisionTestDatabases(cwd, json) {
69
+ registerTsResolve();
70
+ let config;
71
+ try {
72
+ config = await loadGaonConfig(cwd);
73
+ }
74
+ catch {
75
+ return true; // config 로드 실패 = 최소 프로젝트 · DB 준비 스킵
76
+ }
77
+ const dbs = config.db ? Object.entries(config.db) : [];
78
+ if (dbs.length === 0)
79
+ return true;
80
+ const prepared = [];
81
+ try {
82
+ for (const [key, cfg] of dbs) {
83
+ const testCfg = deriveTestDatabaseConfig(cfg);
84
+ await ensureTestDatabaseExists(testCfg);
85
+ const res = await runDbMigrate({
86
+ cwd,
87
+ dbKey: key,
88
+ json: false,
89
+ dryRun: false,
90
+ connectionOverride: testCfg,
91
+ });
92
+ if (res.exitCode !== 0) {
93
+ if (!json)
94
+ process.stderr.write(` ✗ 테스트 DB '${key}' 마이그레이션 실패\n${res.text}\n`);
95
+ return false;
96
+ }
97
+ prepared.push(key);
98
+ }
99
+ }
100
+ catch (err) {
101
+ const msg = err instanceof Error ? err.message : String(err);
102
+ const hint = ` ✗ 테스트 DB 준비 실패: ${msg}\n` +
103
+ ` → DB 가 떠 있는지 확인하세요(docker compose up -d db). 테스트는 실 인프라가 필요합니다(§9).\n`;
104
+ if (json)
105
+ process.stdout.write(JSON.stringify({ ok: false, kind: 'provision', error: msg }) + '\n');
106
+ else
107
+ process.stderr.write(hint);
108
+ return false;
109
+ }
110
+ finally {
111
+ await destroyAllConnections();
112
+ }
113
+ if (json)
114
+ process.stdout.write(JSON.stringify({ kind: 'provisioned', dbs: prepared }) + '\n');
115
+ else
116
+ process.stdout.write(` gaon test · 테스트 DB 준비 완료 (${prepared.join(', ')}) — <db>_test\n`);
117
+ return true;
118
+ }
55
119
  /**
56
120
  * `gaon test` 진입점. args 는 사용자가 넘긴 잔여 인자(필터 문자열 등).
57
121
  * pnpm test 는 pnpm 관례상 `pnpm test -- <args>` 로 넘겨야 vitest 까지 도달.
@@ -60,6 +124,12 @@ export async function runTestCommand(args = [], opts = {}) {
60
124
  const cwd = opts.cwd ?? process.cwd();
61
125
  const scope = opts.scope ?? 'all';
62
126
  const json = opts.json ?? false;
127
+ // 결정 111: unit 이 아니면 실행 전 테스트 DB 를 준비한다(생성 + 마이그레이션).
128
+ if (scope !== 'unit') {
129
+ const ok = await provisionTestDatabases(cwd, json);
130
+ if (!ok)
131
+ return 1;
132
+ }
63
133
  const scopeExtras = scopeArgs(scope);
64
134
  const passthrough = [...scopeExtras, ...args];
65
135
  let cmd;
@@ -1,3 +1,4 @@
1
+ import { type ConnectionConfig } from '@gaonjs/data';
1
2
  import { listMigrationFiles } from './replay.js';
2
3
  export interface DbMigrateOptions {
3
4
  readonly cwd: string;
@@ -7,6 +8,8 @@ export interface DbMigrateOptions {
7
8
  /** `gaon db migrate down` — 가장 최근 이력 한 건 롤백. */
8
9
  readonly down?: boolean;
9
10
  readonly configPath?: string;
11
+ /** 명시 커넥션 오버라이드(결정 111 · gaon test 의 <db>_test). */
12
+ readonly connectionOverride?: ConnectionConfig;
10
13
  }
11
14
  export interface DbMigrateResult {
12
15
  readonly exitCode: number;
@@ -41,6 +41,7 @@ export async function runDbMigrate(opts) {
41
41
  cwd: opts.cwd,
42
42
  dbKey: opts.dbKey,
43
43
  configPath: opts.configPath,
44
+ connectionOverride: opts.connectionOverride,
44
45
  });
45
46
  try {
46
47
  if (opts.down)
@@ -1,4 +1,4 @@
1
- import { type TableDef } from '@gaonjs/data';
1
+ import { type ConnectionConfig, type TableDef } from '@gaonjs/data';
2
2
  import type { Kysely } from 'kysely';
3
3
  import { type Dialect, type AdapterName } from '@gaonjs/data';
4
4
  export interface ResolveDbOptions {
@@ -6,6 +6,12 @@ export interface ResolveDbOptions {
6
6
  readonly dbKey: string;
7
7
  /** --config <path> 로 사용자가 지정한 config 경로. 없으면 cwd 관례. */
8
8
  readonly configPath?: string;
9
+ /**
10
+ * 명시 커넥션 설정 오버라이드(결정 111). 주면 config.db·GAON_DATABASE_URL 보다
11
+ * 우선한다 — `gaon test` 가 <db>_test 로 마이그레이션을 돌릴 때 쓴다. 스키마 스캔
12
+ * (tables)은 여전히 config/cwd 관례를 따른다.
13
+ */
14
+ readonly connectionOverride?: ConnectionConfig;
9
15
  }
10
16
  export interface ResolvedDbTarget {
11
17
  readonly dbKey: string;
@@ -92,7 +92,11 @@ export async function resolveDbTarget(opts) {
92
92
  const dbKey = opts.dbKey;
93
93
  const cfg = config.db?.[dbKey];
94
94
  let connCfg;
95
- if (cfg) {
95
+ if (opts.connectionOverride) {
96
+ // 결정 111: 명시 오버라이드(gaon test 의 <db>_test) 최우선.
97
+ connCfg = opts.connectionOverride;
98
+ }
99
+ else if (cfg) {
96
100
  connCfg = cfg;
97
101
  }
98
102
  else if (dbKey === 'main') {
package/dist/index.js CHANGED
@@ -100,7 +100,7 @@ function renderHelp(version = VERSION) {
100
100
  " gaon serve --workers <n|auto> node:cluster 워커 다중화 (env WEB_CONCURRENCY · 기본 1)",
101
101
  " gaon check typecheck · vue-tsc · build 통합 검사 (--only <step> · --include-doctor)",
102
102
  " gaon console 프로젝트 컨텍스트 REPL (--no-config)",
103
- " gaon test 테스트 러너 (--scope unit|integration|all · -- vitest 인자)",
103
+ " gaon test 테스트 러너 (테스트 DB <db>_test 자동 생성·마이그레이션 후 vitest · --scope unit|integration|all · -- vitest 인자)",
104
104
  " gaon doctor 정적 검사 (22 검사 · 응답 혼용·N+1·의존·커넥션·마이그·컴포저블 순수·자동 import·파일명/컬럼 관례·인증 배선·UI 킷 배선·라우트 등록·정적 충돌·_method·CSRF 배선·내부 앵커·pageProps 구조분해·비동기 오프로드·페이지 레이아웃 브레이크포인트)",
105
105
  " gaon doctor --json 자동화용 JSON 출력",
106
106
  " gaon doctor --check=n-plus-one,connections 선택 검사만 실행",
@@ -0,0 +1,29 @@
1
+ // 웹 앱 팩토리 — gaon g auth 스캐폴드. createWebApp 으로 세션·인증을 배선한다.
2
+ import { createApp, type AppSessionOptions } from 'gaonjs/web'
3
+ import appRoutes from './routes.js'
4
+ import session from './controllers/session.js'
5
+ import registration from './controllers/registration.js'
6
+ import dashboard from './controllers/dashboard.js'
7
+ import { loadUser } from './auth.js'
8
+
9
+ export interface WebAppDeps {
10
+ /** 세션 설정 — { redisUrl, secret } (또는 redis 인스턴스). */
11
+ readonly session: AppSessionOptions
12
+ /** 서명 쿠키/CSRF 용 비밀. */
13
+ readonly cookieSecret?: string
14
+ }
15
+
16
+ export function createWebApp(deps: WebAppDeps) {
17
+ return createApp({
18
+ apps: [
19
+ {
20
+ name: '{{APP_NAME}}',
21
+ routes: appRoutes,
22
+ controllers: { session, registration, dashboard },
23
+ session: deps.session,
24
+ auth: { loadUser, loginRedirect: '/session/new' },
25
+ },
26
+ ],
27
+ cookieSecret: deps.cookieSecret,
28
+ })
29
+ }
@@ -0,0 +1,14 @@
1
+ // 서버 진입점 — gaon g auth 스캐폴드. `node dist/server.js` 로 실행.
2
+ import { createWebApp } from './apps/{{APP_NAME}}/app.js'
3
+
4
+ const app = await createWebApp({
5
+ session: {
6
+ redisUrl: process.env.REDIS_URL ?? 'redis://127.0.0.1:6379',
7
+ secret: process.env.SESSION_SECRET ?? 'change-me-to-a-32+char-random-secret!!',
8
+ },
9
+ cookieSecret: process.env.COOKIE_SECRET,
10
+ })
11
+
12
+ const port = Number(process.env.PORT ?? 3000)
13
+ await app.listen({ port, host: '0.0.0.0' })
14
+ console.log(`web 앱이 http://localhost:${port} 에서 실행 중입니다.`)
@@ -179,9 +179,9 @@ export const posts = table('posts', {
179
179
  | `where` | `(col, op, val?)` | `Chain` | op 에 따라 val 형태 강제 (위 표) |
180
180
  | `whereIn` | `(col, vals)` | `Chain` | `where(col, 'in', vals)` 축약 |
181
181
  | `orWhere` | `(col, op, val?)` | `Chain` | op 12종 전부 (M2C) · 결합은 `(a AND b) OR c` (Rails 관습 · `model.ts:245-262`) |
182
- | `orderBy` | `(col, dir?)` | `Chain` | dir 기본 `'asc'` · 호출마다 누적 (다중 정렬) |
182
+ | `orderBy` | `(col, dir?)` | `Chain` | dir 기본 `'asc'` · 호출마다 누적 (다중 정렬) · 정렬 뒤 PK 타이브레이커 자동 부가(결정 110) |
183
183
  | `reorder` | `(col, dir?)` | `Chain` | 기존 정렬 전부 버리고 재지정 |
184
- | `latest` | `()` | `Chain` | `orderBy('createdAt', 'desc')` 고정 축약 |
184
+ | `latest` | `()` | `Chain` | `orderBy('createdAt', 'desc')` 고정 축약 · id 타이브레이커로 결정적(결정 110) |
185
185
  | `limit` | `(n)` | `Chain` | |
186
186
  | `offset` | `(n)` | `Chain` | 페이지네이션 = `orderBy·offset·limit` 조합 |
187
187
  | `first` | `()` | `Promise<Rec \| undefined>` | 자동 `limit 1` |
@@ -420,6 +420,10 @@ async create() {
420
420
  - `pick()` 결과도 폼이다 — 부분집합으로 다시 `pick()` 할 수 있다.
421
421
  - 스키마에 없는 컬럼을 지정하면 즉시 throw(수리 안내 포함) · 빈
422
422
  `pick()` 도 throw · 중복 지정은 조용히 하나로 합친다.
423
+ - **정적 default 컬럼은 빈 입력이면 그 default 로 채워진다**(결정 108) —
424
+ `t.string().default('N/A')` 같은 컬럼을 pick 해 빈 값('')·미전송으로 받으면
425
+ 런타임이 default 를 채운다(폼 타입이 required 인데 undefined 로 새지 않는다).
426
+ 동적 default(`now()`·`gen_random_uuid()`·bigserial)는 채우지 않고 DB 가 채운다.
423
427
  - `omit`·`extend`·`merge` 는 **없다** — 폼 변형은 `pick()` 하나가 The One Way.
424
428
 
425
429
  ### 9. 서비스 (`service()`) — 트랜잭션 작업 흐름 (정본 §5.3 · `packages/data/src/service.ts`)
@@ -646,4 +650,6 @@ await Post.upsert({ id, title, body }) // onConflict 생략 = 기
646
650
  | 결정 46 | doctor 컬럼(column-casing)·모델/페이지 파일명 검사 3종 |
647
651
  | 결정 47 | `gaon g model` 다단어 테이블명 snake_case(마지막 단어 복수) |
648
652
  | 결정 104 | 스키마 파생 폼에 name·defs 탑재(실검증) · `Model.form.pick()` 검증되는 부분 폼(§8.1) |
653
+ | 결정 108 | 정적 default 컬럼 빈 입력 채움(§8.1) · 동적 default 는 DB 위임 |
654
+ | 결정 110 | `latest()`·`orderBy` 정렬 뒤 PK 타이브레이커 자동 부가(결정적 페이지네이션 · §4) |
649
655
  | E-4 | 컬럼 타입·수식어·체이닝 확장 · `Post.query()` 정정 · Serialized 명명 |
@@ -252,7 +252,9 @@ import PageShell from '@shared/components/ui/PageShell.vue'
252
252
  - **폼은 UI 킷 Form + gaonjs `useForm`(결정 64)** — `Form` 은 얇은 `<form>` 래퍼로
253
253
  `@submit` 을 `useForm` 의 `post/put/delete` 로 넘긴다. vee-validate 를 끌어오지
254
254
  않는다(검증·상태는 `useForm`). `FormField label error` + `FormMessage` 로 라벨·
255
- 오류를 붙이고, `:error="form.errors.<field>"` 로 서버 검증을 표시한다.
255
+ 오류를 붙이고, `:error="form.errors.<field>"` 로 서버 검증을 표시한다 —
256
+ 서버 스키마 검증 실패는 `form.errors.<field>` 로 **자동 반영**된다(결정 109 ·
257
+ 컨트롤러가 손으로 다시 렌더하지 않는다 · `agents/web.md` §4.1).
256
258
  - **`class` 는 폴스루로 병합** — 단일 루트 컴포넌트는 `<Button class="w-full">` 처럼
257
259
  넘긴 클래스가 루트로 흘러간다(별도 `class` prop 선언 없음). `cn` 은 충돌 클래스
258
260
  자동 해소를 하지 않는다 — 오버라이드가 잦으면 tailwind-merge 를 설치해 `cn` 만 교체.
@@ -364,4 +366,5 @@ async function runSearch(q: string) {
364
366
  | 결정 105 | UI 킷 shared 이전(`shared/components/ui` 프로젝트당 한 벌 · `@shared` alias · 결정 75 개정) |
365
367
  | 결정 106 | 최소 4블록(PageShell·PageHeader·EmptyState·Pagination · 성격 중립) |
366
368
  | 결정 107 | 반응형은 킷 책임(페이지 레이아웃 브레이크포인트 지양 · doctor page-layout-breakpoint 안내 경고 · 터치 44px·폰트 최소 크기 토큰) |
369
+ | 결정 109 | 서버 스키마 검증 실패 → `form.errors.<field>` 자동 반영(303 back + 플래시 · `agents/web.md` §4.1) |
367
370
  | E-3 §C | 타입드 `api()` 클라이언트 (routes.d.ts 브리지 재사용) |
@@ -16,9 +16,12 @@
16
16
  테스트를 통과해 운영에서 터지는 구멍이었다.
17
17
 
18
18
  - 테스트 전에 compose 로 DB · NATS 를 띄우고, **테스트 전용
19
- 데이터베이스**(트랜잭션 롤백 격리) + **테스트 전용 스트림 프리픽스**
20
- 쓴다.
21
- - `gaon test` compose 의 테스트 전용 데이터베이스를 자동 준비한다.
19
+ 데이터베이스**(truncate 격리 · 결정 111) + **테스트 전용 스트림
20
+ 프리픽스**를 쓴다.
21
+ - `gaon test` 테스트 전용 데이터베이스(`<db>_test`)를 **자동 준비**한다
22
+ — 없으면 만들고(CREATE DATABASE) 마이그레이션까지 적용한 뒤 vitest 를
23
+ 돌린다. 스캐폴드 `test/setup.ts` 가 그 DB 에 붙고(`connectTestDatabase`)
24
+ 매 테스트 뒤 전 테이블을 비운다(`truncateAll`). 아래 §5 참고.
22
25
  - SQLite 는 Docker 가 불가능한 환경의 폴백으로만 남고 공식 경로가
23
26
  아니다.
24
27
 
@@ -78,11 +81,54 @@ describe('SendWelcomeMail (실 NATS JetStream)', () => {
78
81
  - `configureJobs` 는 헬퍼가 대신 해 준다 — 테스트가 부팅 코드를 흉내낼
79
82
  필요가 없다.
80
83
 
84
+ ### 5. DB 테스트 격리 — `gaon test` + `test/setup.ts` (결정 111)
85
+
86
+ DB 테스트는 손으로 커넥션을 배선하지 않는다 — `gaon test` 와 스캐폴드
87
+ `test/setup.ts` 가 The One Way 를 제공한다:
88
+
89
+ - `gaon test` 가 테스트 전용 DB(`<db>_test`)를 만들고 마이그레이션한다.
90
+ - `test/setup.ts` 가 그 DB 에 붙고(`connectTestDatabase`) 매 테스트 뒤
91
+ 전 테이블을 비운다(`truncateAll`) — 새 테스트는 항상 빈 DB 에서 시작한다.
92
+
93
+ 스캐폴드가 심어 주는 `test/setup.ts`(수정 불필요):
94
+
95
+ ```ts
96
+ import { afterAll, afterEach, beforeAll } from 'vitest'
97
+ import { connectTestDatabase, truncateAll, type TestDbHandle } from 'gaonjs/testing'
98
+
99
+ let handle: TestDbHandle
100
+ beforeAll(async () => { handle = await connectTestDatabase() })
101
+ afterEach(async () => { await truncateAll() })
102
+ afterAll(async () => { await handle?.close() })
103
+ ```
104
+
105
+ 그러면 테스트는 격리 코드 없이 모델·서비스를 그대로 부른다:
106
+
107
+ ```ts
108
+ // test/posts.integration.test.ts
109
+ import { describe, it, expect } from 'vitest'
110
+ import { Post } from '../domain/models/Post.js'
111
+
112
+ describe('Post', () => {
113
+ it('생성·조회', async () => {
114
+ await Post.create({ title: '첫 글', body: '...' })
115
+ expect(await Post.count()).toBe(1n) // 다음 테스트 전 truncateAll 로 0 으로 리셋
116
+ })
117
+ })
118
+ ```
119
+
120
+ **왜 truncate 인가(트랜잭션 롤백 아님) · 결정 111**: `service()` 는 실제
121
+ BEGIN/COMMIT 을 여는 대상이라, 테스트를 바깥 트랜잭션으로 감싸면 service
122
+ 안의 COMMIT 이 그 바깥 트랜잭션을 커밋해 롤백 격리가 조용히 깨진다(실측).
123
+ 그래서 격리는 service 가 실제로 커밋하는 운영 경로를 그대로 두고 매 테스트
124
+ 뒤 truncate 로 비운다 — service 든 아니든 항상 안전하다.
125
+
81
126
  ## 정본 예시
82
127
 
83
- 위 §4 의 `welcomeMail.integration.test.ts` 가 잡 검증의 정본 예시다.
84
- 발행 도착만 확인하고 싶으면 raw JetStream 구독(§3)도 정합이지만, 기본
85
- 경로는 `expectJobProcessed` 하나다 (The One Way).
128
+ 위 §4 의 `welcomeMail.integration.test.ts` 가 잡 검증의 정본 예시이고,
129
+ §5 `test/setup.ts` + 모델 테스트가 DB 격리의 정본 예시다. 발행 도착만
130
+ 확인하고 싶으면 raw JetStream 구독(§3)도 정합이지만, 기본 경로는
131
+ `expectJobProcessed` 하나다 (The One Way).
86
132
 
87
133
  ## 알려진 함정
88
134
 
@@ -99,5 +145,6 @@ describe('SendWelcomeMail (실 NATS JetStream)', () => {
99
145
  | 결정 | 내용 |
100
146
  |---|---|
101
147
  | 결정 42 | 비동기 테스트 헬퍼 `expectJobProcessed` (`gaonjs/testing`) |
148
+ | 결정 111 | `gaon test` 테스트 DB 자동 준비 + `connectTestDatabase`·`truncateAll` 격리(truncate · service COMMIT 실측) |
102
149
  | §9 (v0.15) | 실 인프라 필수 · 목업/인메모리 금지 |
103
150
  | 결정 32 | 잡 발행 위치 자유 — publish 함수가 서비스 경유여도 검증 대상 |
@@ -178,6 +178,31 @@ router.delete('/session', { headers: { 'x-csrf-token': props.csrf } })
178
178
  `?_method=DELETE` 같은 우회는 **서버가 해석하지 않는다** — POST 로 나가
179
179
  엉뚱한 액션(create)에 도달한다.
180
180
 
181
+ ### 4.1 폼 검증 에러 — 스키마 검증만으로 `useForm.errors` 자동 반영 (결정 109)
182
+
183
+ Inertia 폼(`useForm(...).post()`)의 검증 실패는 컨트롤러가 손으로 다시
184
+ 렌더하지 않는다 — `this.params(Model.form)` 이 던진 검증 실패를 프레임웍이
185
+ **303 back + 세션 플래시 errors** 로 처리하고, 클라이언트 `useForm` 이 다음
186
+ 방문의 `errors` 로 `form.errors.<필드>` 를 **자동으로 채운다**. 입력값은
187
+ `useForm` 이 그대로 보존한다(재제출 방지). 컨트롤러는 성공 경로만 쓴다:
188
+
189
+ ```ts
190
+ // 컨트롤러 — 검증 실패 분기를 손으로 쓰지 않는다(결정 109).
191
+ async create() {
192
+ const data = this.params(Post.form.pick('title', 'body')) // 실패 시 프레임웍이 303 back
193
+ await Post.create(data)
194
+ return this.redirect('/posts')
195
+ }
196
+ ```
197
+
198
+ ```vue
199
+ <!-- 페이지 — form.errors.<필드> 는 서버 검증 실패 시 자동으로 채워진다. -->
200
+ <input v-model="form.title" />
201
+ <p v-if="form.errors.title">{{ form.errors.title }}</p>
202
+ ```
203
+
204
+ 순수 JSON/API 앱(X-Inertia 아님·세션 없음)은 기존대로 **422 JSON** 을 받는다.
205
+
181
206
  ### 5. 비밀번호 해싱 — `hashPassword` · `verifyPassword` (`gaonjs/web`)
182
207
 
183
208
  회원가입·로그인에서 비밀번호를 다룰 때는 **직접 crypto/bcrypt 를 import 하거나
@@ -345,4 +370,6 @@ export default controller({
345
370
  | 결정 59 | 인증 배선 = `app.config.ts` 의 `session`+`auth(loadUser)` — 없으면 currentUser 영구 null |
346
371
  | 결정 95 (W4) | 폼 모양 2종 — 스키마 파생 `Model.form`(검증) vs 애드혹 `{ _row }`(타입만) · 라우트 파라미터는 둘 다 자동 병합 |
347
372
  | 결정 104 | `Model.form.pick('a','b')` = 검증되는 부분 폼(결정 95 회부 종결) · 폼 변형은 pick 하나(omit/extend/merge 없음) |
373
+ | 결정 108 | 정적 default 컬럼 빈 입력 채움(coerceParams) · 동적 default 는 DB 위임 |
374
+ | 결정 109 | Inertia 폼 검증 실패 = 303 back + 플래시 errors → `useForm.errors` 자동(§4.1) · JSON/API 는 422 유지 |
348
375
  | E-1 | 파사드 = `gaonjs` · CLI = `gaon` |
@@ -0,0 +1,24 @@
1
+ // test/setup.ts — 테스트 격리 부트스트랩 (§9 실 인프라 · 결정 111).
2
+ //
3
+ // `gaon test` 가 테스트 전용 DB(<db>_test)를 만들고 마이그레이션한 뒤 vitest 를
4
+ // 돌린다. 이 파일이 그 DB 에 붙고(connectTestDatabase), 매 테스트 뒤 전 테이블을
5
+ // 비운다(truncateAll). 트랜잭션 롤백이 아니라 truncate 인 이유: service() 는 실제
6
+ // COMMIT 을 해서 바깥 트랜잭션으로 되돌릴 수 없다(결정 111 · agents/testing.md).
7
+ //
8
+ // 목업·인메모리 금지(§9) — 실 DB 로만 검증한다.
9
+ import { afterAll, afterEach, beforeAll } from 'vitest'
10
+ import { connectTestDatabase, truncateAll, type TestDbHandle } from 'gaonjs/testing'
11
+
12
+ let handle: TestDbHandle
13
+
14
+ beforeAll(async () => {
15
+ handle = await connectTestDatabase()
16
+ })
17
+
18
+ afterEach(async () => {
19
+ await truncateAll()
20
+ })
21
+
22
+ afterAll(async () => {
23
+ await handle?.close()
24
+ })
@@ -0,0 +1,23 @@
1
+ // vitest.config.ts — 테스트 러너 설정 (§9 실 인프라 · 결정 111).
2
+ //
3
+ // `gaon test`(= vitest)가 이 설정으로 돈다. 프론트 빌드는 vite.config.ts 가,
4
+ // 테스트는 이 파일이 담당한다(vitest 는 vitest.config 를 우선한다).
5
+ //
6
+ // 격리: `gaon test` 가 테스트 전용 DB(<db>_test)를 만들고 마이그레이션한 뒤,
7
+ // test/setup.ts 가 그 DB 에 붙고 매 테스트 뒤 전 테이블을 비운다(truncate).
8
+ // 실 DB 하나를 공유하므로 파일 병렬을 끈다 — 병렬이면 서로의 데이터를 지운다.
9
+ import { defineConfig } from 'vitest/config'
10
+
11
+ export default defineConfig({
12
+ test: {
13
+ setupFiles: ['./test/setup.ts'],
14
+ include: [
15
+ 'test/**/*.test.ts',
16
+ 'domain/**/*.test.ts',
17
+ 'apps/**/*.test.ts',
18
+ 'shared/**/*.test.ts',
19
+ ],
20
+ // 실 DB 를 공유하는 통합 테스트 격리 — 파일 병렬 금지(§9 · 결정 111).
21
+ fileParallelism: false,
22
+ },
23
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaonjs/cli",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Gaon CLI 구현: 제너레이터·스캐폴딩·로드맵 출력 (M1 스텁)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,12 +27,12 @@
27
27
  "@modelcontextprotocol/sdk": "^1.29.0",
28
28
  "typescript": "^5.9.0",
29
29
  "vite": "^7.0.0",
30
- "@gaonjs/data": "0.10.0",
31
- "@gaonjs/config": "0.5.4",
30
+ "@gaonjs/async": "0.6.1",
31
+ "@gaonjs/config": "0.6.0",
32
32
  "@gaonjs/core": "0.2.1",
33
- "@gaonjs/mail": "0.1.3",
34
- "@gaonjs/web": "0.7.2",
35
- "@gaonjs/async": "0.6.1"
33
+ "@gaonjs/web": "0.8.0",
34
+ "@gaonjs/data": "0.11.0",
35
+ "@gaonjs/mail": "0.1.3"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"require('fs').cpSync('src/templates','dist/templates',{recursive:true})\""