@simplysm/sd-cli 14.0.19 → 14.0.22
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.
- package/dist/angular/vite-postcss-inline-plugin.d.ts.map +1 -1
- package/dist/angular/vite-postcss-inline-plugin.js +4 -1
- package/dist/angular/vite-postcss-inline-plugin.js.map +1 -1
- package/dist/capacitor/capacitor-android.d.ts +16 -0
- package/dist/capacitor/capacitor-android.d.ts.map +1 -0
- package/dist/capacitor/capacitor-android.js +289 -0
- package/dist/capacitor/capacitor-android.js.map +1 -0
- package/dist/capacitor/capacitor.d.ts +0 -49
- package/dist/capacitor/capacitor.d.ts.map +1 -1
- package/dist/capacitor/capacitor.js +4 -244
- package/dist/capacitor/capacitor.js.map +1 -1
- package/dist/commands/check.js +2 -2
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/lint.d.ts +1 -42
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +1 -151
- package/dist/commands/lint.js.map +1 -1
- package/dist/commands/publish.d.ts.map +1 -1
- package/dist/commands/publish.js +2 -1
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/typecheck.d.ts +3 -40
- package/dist/commands/typecheck.d.ts.map +1 -1
- package/dist/commands/typecheck.js +3 -232
- package/dist/commands/typecheck.js.map +1 -1
- package/dist/electron/electron.js +11 -4
- package/dist/electron/electron.js.map +1 -1
- package/dist/engines/ViteEngine.js +1 -1
- package/dist/engines/ViteEngine.js.map +1 -1
- package/dist/engines/types.d.ts +2 -0
- package/dist/engines/types.d.ts.map +1 -1
- package/dist/infra/ResultCollector.d.ts +2 -2
- package/dist/infra/ResultCollector.d.ts.map +1 -1
- package/dist/infra/ResultCollector.js +1 -1
- package/dist/orchestrators/DevWatchOrchestrator.d.ts +2 -1
- package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/DevWatchOrchestrator.js +28 -16
- package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
- package/dist/orchestrators/TypecheckOrchestrator.d.ts +74 -0
- package/dist/orchestrators/TypecheckOrchestrator.d.ts.map +1 -0
- package/dist/orchestrators/TypecheckOrchestrator.js +285 -0
- package/dist/orchestrators/TypecheckOrchestrator.js.map +1 -0
- package/dist/sd-cli.js +6 -1
- package/dist/sd-cli.js.map +1 -1
- package/dist/utils/lint-core.d.ts +43 -0
- package/dist/utils/lint-core.d.ts.map +1 -0
- package/dist/utils/lint-core.js +154 -0
- package/dist/utils/lint-core.js.map +1 -0
- package/dist/utils/lint-utils.d.ts +1 -1
- package/dist/utils/lint-utils.d.ts.map +1 -1
- package/dist/utils/output-utils.d.ts +2 -2
- package/dist/utils/output-utils.d.ts.map +1 -1
- package/dist/utils/output-utils.js.map +1 -1
- package/dist/utils/server-production-files.d.ts +22 -0
- package/dist/utils/server-production-files.d.ts.map +1 -0
- package/dist/utils/server-production-files.js +162 -0
- package/dist/utils/server-production-files.js.map +1 -0
- package/dist/utils/vite-config.js.map +1 -1
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +7 -1
- package/dist/workers/client.worker.js.map +1 -1
- package/dist/workers/lint.worker.d.ts +1 -1
- package/dist/workers/lint.worker.d.ts.map +1 -1
- package/dist/workers/lint.worker.js +1 -1
- package/dist/workers/lint.worker.js.map +1 -1
- package/dist/workers/server-build.worker.d.ts.map +1 -1
- package/dist/workers/server-build.worker.js +12 -161
- package/dist/workers/server-build.worker.js.map +1 -1
- package/package.json +4 -4
- package/src/angular/vite-postcss-inline-plugin.ts +5 -1
- package/src/capacitor/capacitor-android.ts +368 -0
- package/src/capacitor/capacitor.ts +4 -317
- package/src/commands/check.ts +2 -2
- package/src/commands/lint.ts +1 -201
- package/src/commands/publish.ts +2 -1
- package/src/commands/typecheck.ts +7 -292
- package/src/electron/electron.ts +4 -4
- package/src/engines/ViteEngine.ts +1 -1
- package/src/engines/types.ts +3 -0
- package/src/infra/ResultCollector.ts +2 -2
- package/src/orchestrators/DevWatchOrchestrator.ts +35 -20
- package/src/orchestrators/TypecheckOrchestrator.ts +364 -0
- package/src/sd-cli.ts +6 -1
- package/src/utils/lint-core.ts +205 -0
- package/src/utils/lint-utils.ts +1 -1
- package/src/utils/output-utils.ts +3 -3
- package/src/utils/server-production-files.ts +186 -0
- package/src/utils/vite-config.ts +1 -1
- package/src/workers/client.worker.ts +7 -1
- package/src/workers/lint.worker.ts +1 -1
- package/src/workers/server-build.worker.ts +11 -185
- package/tests/angular/vite-postcss-inline-plugin.spec.ts +10 -0
- package/tests/capacitor/capacitor-android-exports.verify.md +11 -0
- package/tests/capacitor/capacitor-android.spec.ts +219 -0
- package/tests/capacitor/capacitor-build.spec.ts +17 -21
- package/tests/capacitor/capacitor-icon.spec.ts +17 -19
- package/tests/capacitor/capacitor-init.spec.ts +18 -14
- package/tests/capacitor/capacitor-run.spec.ts +10 -24
- package/tests/capacitor/capacitor-workspace.spec.ts +10 -15
- package/tests/commands/check.spec.ts +2 -2
- package/tests/commands/lint.spec.ts +33 -194
- package/tests/commands/publish-set.verify.md +7 -0
- package/tests/electron/electron-symlink-cleanup.verify.md +8 -0
- package/tests/engines/vite-engine.spec.ts +29 -0
- package/tests/infra/result-collector.spec.ts +11 -0
- package/tests/orchestrators/dev-watch-orchestrator.spec.ts +70 -0
- package/tests/orchestrators/dist-delete-watcher.verify.md +10 -0
- package/tests/orchestrators/typecheck-orchestrator.spec.ts +180 -0
- package/tests/sd-cli-catch-all.verify.md +7 -0
- package/tests/utils/lint-core-import-paths.verify.md +10 -0
- package/tests/utils/lint-core.spec.ts +188 -0
- package/tests/utils/server-production-files-import-paths.verify.md +14 -0
- package/tests/workers/client-worker.spec.ts +92 -0
- package/tests/workers/server-build-context-dispose.verify.md +8 -0
- package/tests/workers/server-build-worker.spec.ts +39 -0
|
@@ -1,189 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
const mocks = vi.hoisted(() => ({
|
|
5
|
-
fsxExists: vi.fn<(path: string) => Promise<boolean>>(),
|
|
6
|
-
fsxGlob: vi.fn<(...args: unknown[]) => Promise<string[]>>(),
|
|
7
|
-
filterByTargets: vi.fn(
|
|
8
|
-
(files: string[], _targets: string[], _cwd: string) => files,
|
|
9
|
-
),
|
|
10
|
-
lintFiles: vi.fn<() => Promise<Array<{ errorCount: number; warningCount: number }>>>(),
|
|
11
|
-
loadFormatter: vi.fn(),
|
|
12
|
-
outputFixes: vi.fn(),
|
|
13
|
-
jitiImport: vi.fn(),
|
|
14
|
-
eslintCtor: vi.fn(),
|
|
15
|
-
}));
|
|
3
|
+
//#region Mocks
|
|
16
4
|
|
|
17
|
-
vi.
|
|
18
|
-
|
|
19
|
-
pathx: { filterByTargets: mocks.filterByTargets },
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
vi.mock("eslint", () => ({
|
|
23
|
-
ESLint: class MockESLint {
|
|
24
|
-
constructor(options: unknown) { mocks.eslintCtor(options); }
|
|
25
|
-
lintFiles = mocks.lintFiles;
|
|
26
|
-
loadFormatter = mocks.loadFormatter;
|
|
27
|
-
static outputFixes = mocks.outputFixes;
|
|
28
|
-
},
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
executeLint: vi.fn(),
|
|
29
7
|
}));
|
|
30
8
|
|
|
31
|
-
vi.mock("
|
|
32
|
-
|
|
9
|
+
vi.mock("../../src/utils/lint-core", () => ({
|
|
10
|
+
executeLint: mocks.executeLint,
|
|
33
11
|
}));
|
|
34
12
|
|
|
35
|
-
|
|
36
|
-
const fns = (): Record<string, unknown> => ({
|
|
37
|
-
debug: vi.fn(), start: vi.fn(), success: vi.fn(),
|
|
38
|
-
info: vi.fn(), error: vi.fn(), warn: vi.fn(), log: vi.fn(),
|
|
39
|
-
withTag: vi.fn(() => fns()),
|
|
40
|
-
level: 0,
|
|
41
|
-
});
|
|
42
|
-
const c = fns();
|
|
43
|
-
return { consola: c, default: c, LogLevels: {} };
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const { loadIgnorePatterns, executeLint, runLint } = await import("../../src/commands/lint");
|
|
47
|
-
|
|
48
|
-
//#region loadIgnorePatterns
|
|
49
|
-
|
|
50
|
-
describe("loadIgnorePatterns", () => {
|
|
51
|
-
beforeEach(() => vi.clearAllMocks());
|
|
52
|
-
|
|
53
|
-
it("extracts globalIgnores patterns from eslint config", async () => {
|
|
54
|
-
mocks.fsxExists.mockImplementation((p) =>
|
|
55
|
-
Promise.resolve(typeof p === "string" && p.endsWith("eslint.config.ts")),
|
|
56
|
-
);
|
|
57
|
-
mocks.jitiImport.mockResolvedValue({
|
|
58
|
-
default: [
|
|
59
|
-
{ ignores: ["dist/**", "node_modules/**"] },
|
|
60
|
-
{ files: ["*.ts"], rules: {} }, // not globalIgnores — has files key
|
|
61
|
-
{ ignores: [".cache/**"] },
|
|
62
|
-
],
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const result = await loadIgnorePatterns("/project");
|
|
66
|
-
expect(result).toEqual(["dist/**", "node_modules/**", ".cache/**"]);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("ignores config objects that have files key", async () => {
|
|
70
|
-
mocks.fsxExists.mockImplementation((p) =>
|
|
71
|
-
Promise.resolve(typeof p === "string" && p.endsWith("eslint.config.ts")),
|
|
72
|
-
);
|
|
73
|
-
mocks.jitiImport.mockResolvedValue({
|
|
74
|
-
default: [{ files: ["*.ts"], ignores: ["dist/**"] }],
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const result = await loadIgnorePatterns("/project");
|
|
78
|
-
expect(result).toEqual([]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("throws when no eslint config file found", async () => {
|
|
82
|
-
mocks.fsxExists.mockResolvedValue(false);
|
|
83
|
-
|
|
84
|
-
await expect(loadIgnorePatterns("/project")).rejects.toThrow(
|
|
85
|
-
"ESLint 설정 파일",
|
|
86
|
-
);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
//#endregion
|
|
91
|
-
|
|
92
|
-
//#region executeLint
|
|
93
|
-
|
|
94
|
-
describe("executeLint", () => {
|
|
95
|
-
beforeEach(() => {
|
|
96
|
-
vi.clearAllMocks();
|
|
97
|
-
// Default: eslint config exists with no ignores
|
|
98
|
-
mocks.fsxExists.mockImplementation((p) =>
|
|
99
|
-
Promise.resolve(typeof p === "string" && p.endsWith("eslint.config.ts")),
|
|
100
|
-
);
|
|
101
|
-
mocks.jitiImport.mockResolvedValue({ default: [] });
|
|
102
|
-
mocks.fsxGlob.mockResolvedValue(["/project/src/a.ts", "/project/src/b.ts"]);
|
|
103
|
-
mocks.filterByTargets.mockImplementation((files) => files);
|
|
104
|
-
mocks.lintFiles.mockResolvedValue([{ errorCount: 0, warningCount: 0 }]);
|
|
105
|
-
mocks.loadFormatter.mockResolvedValue({
|
|
106
|
-
format: vi.fn().mockResolvedValue(""),
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("lints all collected files and returns success when no errors", async () => {
|
|
111
|
-
const result = await executeLint({ targets: [], fix: false, timing: false });
|
|
112
|
-
|
|
113
|
-
expect(result.success).toBe(true);
|
|
114
|
-
expect(result.errorCount).toBe(0);
|
|
115
|
-
expect(result.warningCount).toBe(0);
|
|
116
|
-
expect(mocks.lintFiles).toHaveBeenCalled();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("filters files by targets via pathx.filterByTargets", async () => {
|
|
120
|
-
const filteredFiles = ["/project/packages/core-common/src/a.ts"];
|
|
121
|
-
mocks.filterByTargets.mockReturnValue(filteredFiles);
|
|
122
|
-
|
|
123
|
-
await executeLint({ targets: ["packages/core-common"], fix: false, timing: false });
|
|
124
|
-
|
|
125
|
-
expect(mocks.filterByTargets).toHaveBeenCalledWith(
|
|
126
|
-
expect.any(Array),
|
|
127
|
-
["packages/core-common"],
|
|
128
|
-
expect.any(String),
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("applies auto-fix when fix option is true", async () => {
|
|
133
|
-
await executeLint({ targets: [], fix: true, timing: false });
|
|
134
|
-
|
|
135
|
-
expect(mocks.outputFixes).toHaveBeenCalled();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("sets TIMING env variable when timing option is true", async () => {
|
|
139
|
-
const origTiming = process.env["TIMING"];
|
|
140
|
-
|
|
141
|
-
await executeLint({ targets: [], fix: false, timing: true });
|
|
142
|
-
|
|
143
|
-
expect(process.env["TIMING"]).toBe("1");
|
|
144
|
-
|
|
145
|
-
// Cleanup
|
|
146
|
-
if (origTiming == null) delete process.env["TIMING"];
|
|
147
|
-
else process.env["TIMING"] = origTiming;
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("creates ESLint with cache enabled and correct cache location", async () => {
|
|
151
|
-
await executeLint({ targets: [], fix: false, timing: false });
|
|
152
|
-
|
|
153
|
-
expect(mocks.eslintCtor).toHaveBeenCalledWith(
|
|
154
|
-
expect.objectContaining({
|
|
155
|
-
cache: true,
|
|
156
|
-
cacheLocation: expect.stringContaining("eslint.cache"),
|
|
157
|
-
}),
|
|
158
|
-
);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("returns error count and formatted output when lint errors found", async () => {
|
|
162
|
-
mocks.lintFiles.mockResolvedValue([
|
|
163
|
-
{ errorCount: 3, warningCount: 1 },
|
|
164
|
-
{ errorCount: 1, warningCount: 2 },
|
|
165
|
-
]);
|
|
166
|
-
mocks.loadFormatter.mockResolvedValue({
|
|
167
|
-
format: vi.fn().mockResolvedValue("error details"),
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const result = await executeLint({ targets: [], fix: false, timing: false });
|
|
171
|
-
|
|
172
|
-
expect(result.success).toBe(false);
|
|
173
|
-
expect(result.errorCount).toBe(4);
|
|
174
|
-
expect(result.warningCount).toBe(3);
|
|
175
|
-
expect(result.formattedOutput).toBe("error details");
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("returns success when no files to lint", async () => {
|
|
179
|
-
mocks.fsxGlob.mockResolvedValue([]);
|
|
180
|
-
|
|
181
|
-
const result = await executeLint({ targets: [], fix: false, timing: false });
|
|
182
|
-
|
|
183
|
-
expect(result.success).toBe(true);
|
|
184
|
-
expect(mocks.lintFiles).not.toHaveBeenCalled();
|
|
185
|
-
});
|
|
186
|
-
});
|
|
13
|
+
const { runLint } = await import("../../src/commands/lint");
|
|
187
14
|
|
|
188
15
|
//#endregion
|
|
189
16
|
|
|
@@ -200,15 +27,11 @@ describe("runLint", () => {
|
|
|
200
27
|
writeSpy = vi.spyOn(process.stdout, "write").mockReturnValue(true);
|
|
201
28
|
|
|
202
29
|
// Default: successful lint
|
|
203
|
-
mocks.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
mocks.filterByTargets.mockImplementation((files) => files);
|
|
209
|
-
mocks.lintFiles.mockResolvedValue([{ errorCount: 0, warningCount: 0 }]);
|
|
210
|
-
mocks.loadFormatter.mockResolvedValue({
|
|
211
|
-
format: vi.fn().mockResolvedValue(""),
|
|
30
|
+
mocks.executeLint.mockResolvedValue({
|
|
31
|
+
success: true,
|
|
32
|
+
errorCount: 0,
|
|
33
|
+
warningCount: 0,
|
|
34
|
+
formattedOutput: "",
|
|
212
35
|
});
|
|
213
36
|
});
|
|
214
37
|
|
|
@@ -218,9 +41,11 @@ describe("runLint", () => {
|
|
|
218
41
|
});
|
|
219
42
|
|
|
220
43
|
it("writes formatted output to stdout when there are results", async () => {
|
|
221
|
-
mocks.
|
|
222
|
-
|
|
223
|
-
|
|
44
|
+
mocks.executeLint.mockResolvedValue({
|
|
45
|
+
success: false,
|
|
46
|
+
errorCount: 1,
|
|
47
|
+
warningCount: 0,
|
|
48
|
+
formattedOutput: "lint output here",
|
|
224
49
|
});
|
|
225
50
|
|
|
226
51
|
await runLint({ targets: [], fix: false, timing: false });
|
|
@@ -229,15 +54,29 @@ describe("runLint", () => {
|
|
|
229
54
|
});
|
|
230
55
|
|
|
231
56
|
it("sets exitCode to 1 when lint errors are found", async () => {
|
|
232
|
-
mocks.
|
|
233
|
-
|
|
234
|
-
|
|
57
|
+
mocks.executeLint.mockResolvedValue({
|
|
58
|
+
success: false,
|
|
59
|
+
errorCount: 1,
|
|
60
|
+
warningCount: 0,
|
|
61
|
+
formattedOutput: "errors",
|
|
235
62
|
});
|
|
236
63
|
|
|
237
64
|
await runLint({ targets: [], fix: false, timing: false });
|
|
238
65
|
|
|
239
66
|
expect(process.exitCode).toBe(1);
|
|
240
67
|
});
|
|
68
|
+
|
|
69
|
+
it("does not set exitCode when lint passes", async () => {
|
|
70
|
+
await runLint({ targets: [], fix: false, timing: false });
|
|
71
|
+
|
|
72
|
+
expect(process.exitCode).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("does not write to stdout when formattedOutput is empty", async () => {
|
|
76
|
+
await runLint({ targets: [], fix: false, timing: false });
|
|
77
|
+
|
|
78
|
+
expect(writeSpy).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
241
80
|
});
|
|
242
81
|
|
|
243
82
|
//#endregion
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# 배포 실패 패키지 ���색 Set 사용 -- LLM 검증
|
|
2
|
+
|
|
3
|
+
## 검증 항목
|
|
4
|
+
|
|
5
|
+
- [x] Set 생성: `publish.ts:773` — `const publishedSet = new Set(publishedPackages)` 확인
|
|
6
|
+
- [x] Set.has() 사용: `publish.ts:774` — `allPkgNames.filter(n => !publishedSet.has(n))` 확인
|
|
7
|
+
- [x] 동작 동등성: `Array.includes()` → `Set.has()` 변환은 동일한 boolean 결과를 반환하���로 기능 동등
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# symlink 테스트 임시 파일 정리 -- LLM 검증
|
|
2
|
+
|
|
3
|
+
## 검증 항목
|
|
4
|
+
|
|
5
|
+
- [x] try-catch-finally 구조 적용: `electron.ts:348-358` — try 블록에서 writeFile/symlink/lstat 수행, finally에서 정리
|
|
6
|
+
- [x] finally에서 testLink, testTarget 각각 unlink: `finally { try { fs.unlinkSync(testLink); } catch {} try { fs.unlinkSync(testTarget); } catch {} }` 확인
|
|
7
|
+
- [x] 성공 시에도 파일 정리: try 블록 return 후 finally가 실행되므로 정리 보장
|
|
8
|
+
- [x] 실패 시에도 파일 정리: catch 블록 return 후 finally가 실행되므로 정리 보장
|
|
@@ -492,6 +492,35 @@ describe("ViteEngine", () => {
|
|
|
492
492
|
await engine.stop();
|
|
493
493
|
});
|
|
494
494
|
|
|
495
|
+
// Unit: ViteEngine uses same workerKey pattern as BaseEngine (CONSIST-001)
|
|
496
|
+
it("registers with RebuildManager using '{name}:build' key pattern", async () => {
|
|
497
|
+
const mockRebuildManager = { registerBuild: vi.fn(() => vi.fn()) };
|
|
498
|
+
|
|
499
|
+
mockWorker.startWatch.mockImplementation(() => {
|
|
500
|
+
return Promise.resolve({ success: true });
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const engine = new ViteEngine({
|
|
504
|
+
cwd: "/root",
|
|
505
|
+
pkg: createMockPkg({ name: "my-client" }),
|
|
506
|
+
rebuildManager: mockRebuildManager as any,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
await engine.startWatch({ js: true, dts: false });
|
|
510
|
+
|
|
511
|
+
const buildStartHandler = mockWorker.on.mock.calls.find(
|
|
512
|
+
(call: any[]) => call[0] === "buildStart",
|
|
513
|
+
)?.[1];
|
|
514
|
+
buildStartHandler?.({});
|
|
515
|
+
|
|
516
|
+
expect(mockRebuildManager.registerBuild).toHaveBeenCalledWith(
|
|
517
|
+
"my-client:build",
|
|
518
|
+
expect.any(String),
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
await engine.stop();
|
|
522
|
+
});
|
|
523
|
+
|
|
495
524
|
// Acceptance: Scenario "build 이벤트로 ResultCollector 갱신 + resolver 호출" (Feature 3.3)
|
|
496
525
|
it("resolves rebuild when build event arrives after buildStart", async () => {
|
|
497
526
|
const mockResolver = vi.fn();
|
|
@@ -43,4 +43,15 @@ describe("ResultCollector", () => {
|
|
|
43
43
|
expect(collector.get("storage:server")).toBeDefined();
|
|
44
44
|
expect(collector.get("storage:build")).toBeUndefined();
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
it("toMap returns a map that does not allow external mutation of internal state (DESIGN-004)", () => {
|
|
48
|
+
const collector = new ResultCollector();
|
|
49
|
+
collector.add({ name: "core", target: "node", type: "build", status: "success" });
|
|
50
|
+
|
|
51
|
+
const map = collector.toMap();
|
|
52
|
+
// ReadonlyMap at compile time; at runtime, verify internal state isolation
|
|
53
|
+
expect(map.size).toBe(1);
|
|
54
|
+
// Internal state should still have the result regardless of external map reference
|
|
55
|
+
expect(collector.get("core:build")!.status).toBe("success");
|
|
56
|
+
});
|
|
46
57
|
});
|
|
@@ -1471,6 +1471,76 @@ describe("DevWatchOrchestrator", () => {
|
|
|
1471
1471
|
|
|
1472
1472
|
//#region Slice 4: watch/dev lint 활성화 (Feature 3.2)
|
|
1473
1473
|
|
|
1474
|
+
describe("resource safety (DESIGN-001, DESIGN-002)", () => {
|
|
1475
|
+
// --- Acceptance: shutdown 시 타이머 정리 + replaceDepWatcher 해제 ---
|
|
1476
|
+
|
|
1477
|
+
it("clears pending timers on shutdown so no delayed restart fires", async () => {
|
|
1478
|
+
vi.useFakeTimers();
|
|
1479
|
+
try {
|
|
1480
|
+
setupDefaults(createConfig({
|
|
1481
|
+
packages: { "demo-server": { target: "server" } },
|
|
1482
|
+
}));
|
|
1483
|
+
// Engine adds "success" result to trigger restart timer via batchComplete
|
|
1484
|
+
vi.mocked(createBuildEngine).mockImplementation((pkg: any, options: any) => {
|
|
1485
|
+
const engine = {
|
|
1486
|
+
run: vi.fn(),
|
|
1487
|
+
startWatch: vi.fn().mockImplementation(() => {
|
|
1488
|
+
options.resultCollector?.add({
|
|
1489
|
+
name: pkg.name, target: "server", type: "build", status: "success",
|
|
1490
|
+
});
|
|
1491
|
+
}),
|
|
1492
|
+
stop: vi.fn().mockResolvedValue(undefined),
|
|
1493
|
+
_pkgName: pkg.name,
|
|
1494
|
+
};
|
|
1495
|
+
mockBuildEngines.push(engine);
|
|
1496
|
+
return engine as any;
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
const orchestrator = new DevWatchOrchestrator({ mode: "dev", targets: [], options: [] });
|
|
1500
|
+
await orchestrator.initialize();
|
|
1501
|
+
await orchestrator.start();
|
|
1502
|
+
|
|
1503
|
+
// Trigger batchComplete with a server build key → sets _serverRestartTimer
|
|
1504
|
+
const rebuildInstance = vi.mocked(RebuildManager).mock.instances[0];
|
|
1505
|
+
const onCall = vi.mocked(rebuildInstance.on).mock.calls.find((c) => c[0] === "batchComplete");
|
|
1506
|
+
const batchHandler = onCall?.[1] as ((keys: string[]) => void) | undefined;
|
|
1507
|
+
const runtimeCountBefore = mockRuntimeProxies.length;
|
|
1508
|
+
batchHandler?.(["demo-server:build"]);
|
|
1509
|
+
|
|
1510
|
+
// shutdown before timer fires
|
|
1511
|
+
await orchestrator.shutdown();
|
|
1512
|
+
|
|
1513
|
+
// Advance past timer (100ms restart + 300ms print)
|
|
1514
|
+
vi.advanceTimersByTime(500);
|
|
1515
|
+
|
|
1516
|
+
// No new runtime workers created after shutdown
|
|
1517
|
+
expect(mockRuntimeProxies.length).toBe(runtimeCountBefore);
|
|
1518
|
+
} finally {
|
|
1519
|
+
vi.useRealTimers();
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
it("disposes replaceDepWatcher even when initialize fails after watchReplaceDeps", async () => {
|
|
1524
|
+
const mockDispose = vi.fn();
|
|
1525
|
+
vi.mocked(watchReplaceDeps).mockResolvedValue({ entries: [], dispose: mockDispose } as any);
|
|
1526
|
+
|
|
1527
|
+
setupDefaults(createConfig({
|
|
1528
|
+
packages: { "demo-server": { target: "server" } },
|
|
1529
|
+
replaceDeps: { "@simplysm/*": "packages/*/src" },
|
|
1530
|
+
}));
|
|
1531
|
+
vi.mocked(watchReplaceDeps).mockResolvedValue({ entries: [], dispose: mockDispose } as any);
|
|
1532
|
+
// Make getVersion throw to simulate partial init failure after watchReplaceDeps
|
|
1533
|
+
vi.mocked(getVersion).mockRejectedValue(new Error("version fetch failed"));
|
|
1534
|
+
|
|
1535
|
+
const orchestrator = new DevWatchOrchestrator({ mode: "dev", targets: [], options: [] });
|
|
1536
|
+
await expect(orchestrator.initialize()).rejects.toThrow("version fetch failed");
|
|
1537
|
+
|
|
1538
|
+
await orchestrator.shutdown();
|
|
1539
|
+
|
|
1540
|
+
expect(mockDispose).toHaveBeenCalledOnce();
|
|
1541
|
+
});
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1474
1544
|
describe("lint activation", () => {
|
|
1475
1545
|
// Scenario: watch 초기 빌드에서 lint 비활성화 (별도 실행으로 분리됨)
|
|
1476
1546
|
it("passes lint:false to startWatch for library engines in watch mode", async () => {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# dist 삭제 감지 watcher 일반화 -- LLM 검증
|
|
2
|
+
|
|
3
|
+
## 검증 항목
|
|
4
|
+
|
|
5
|
+
- [x] 디버그 하드코딩 제거: `DevWatchOrchestrator.ts:281` — `angular` 하드코딩 블록이 제거됨
|
|
6
|
+
- [x] 모든 라이브러리 패키지 감시: `_startWatchMode()`에서 `this._libraryPackages`를 순회하며 각 `pkg.dir/dist`를 감시
|
|
7
|
+
- [x] 클래스 필드 저장: `_distDeleteWatchers: FsWatcher[]` 필드에 push
|
|
8
|
+
- [x] shutdown() 정리: `shutdown()`에서 `this._distDeleteWatchers.map(w => w.close())` 호출 확인
|
|
9
|
+
- [x] 정리 후 초기화: `this._distDeleteWatchers = []`로 참조 해제 확인
|
|
10
|
+
- [x] 로그 형식: `[dist-delete:{패키지명}]` 형식으로 어떤 패키지의 dist가 삭제되었는지 식별 가능
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
//#region Mocks
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
loadSdConfig: vi.fn(),
|
|
7
|
+
deserializeDiagnostic: vi.fn((d: any) => d),
|
|
8
|
+
typecheckNonPackageFiles: vi.fn(),
|
|
9
|
+
createBuildEngine: vi.fn(),
|
|
10
|
+
discoverWorkspacePackages: vi.fn(),
|
|
11
|
+
mergeTestsPackagesIntoConfig: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const mockEngines: Array<{
|
|
15
|
+
run: ReturnType<typeof vi.fn>;
|
|
16
|
+
stop: ReturnType<typeof vi.fn>;
|
|
17
|
+
}> = [];
|
|
18
|
+
|
|
19
|
+
vi.mock("../../src/utils/sd-config", () => ({
|
|
20
|
+
loadSdConfig: mocks.loadSdConfig,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("../../src/utils/typecheck-serialization", () => ({
|
|
24
|
+
deserializeDiagnostic: mocks.deserializeDiagnostic,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("../../src/utils/typecheck-non-package", () => ({
|
|
28
|
+
typecheckNonPackageFiles: mocks.typecheckNonPackageFiles,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock("../../src/engines/index", () => ({
|
|
32
|
+
createBuildEngine: mocks.createBuildEngine,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("../../src/utils/package-utils", async (importOriginal) => {
|
|
36
|
+
const actual = await importOriginal<typeof import("../../src/utils/package-utils")>();
|
|
37
|
+
return {
|
|
38
|
+
...actual,
|
|
39
|
+
discoverWorkspacePackages: mocks.discoverWorkspacePackages,
|
|
40
|
+
mergeTestsPackagesIntoConfig: mocks.mergeTestsPackagesIntoConfig,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
vi.mock("typescript", async (importOriginal) => {
|
|
45
|
+
const orig = await importOriginal<Record<string, unknown>>();
|
|
46
|
+
const origDefault = (orig["default"] ?? orig) as Record<string, unknown>;
|
|
47
|
+
return {
|
|
48
|
+
...orig,
|
|
49
|
+
default: {
|
|
50
|
+
...origDefault,
|
|
51
|
+
sortAndDeduplicateDiagnostics: vi.fn((d: unknown[]) => d),
|
|
52
|
+
formatDiagnosticsWithColorAndContext: vi.fn((diags: Array<{ messageText: string }>) =>
|
|
53
|
+
diags.map((d) => `formatted: ${d.messageText}`).join("\n"),
|
|
54
|
+
),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
vi.mock("consola", () => {
|
|
60
|
+
const fns = (): Record<string, unknown> => ({
|
|
61
|
+
debug: vi.fn(), start: vi.fn(), success: vi.fn(),
|
|
62
|
+
info: vi.fn(), error: vi.fn(), warn: vi.fn(), log: vi.fn(),
|
|
63
|
+
withTag: vi.fn(() => fns()),
|
|
64
|
+
level: 0,
|
|
65
|
+
});
|
|
66
|
+
const c = fns();
|
|
67
|
+
return { consola: c, default: c, LogLevels: {} };
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const { TypecheckOrchestrator } = await import(
|
|
71
|
+
"../../src/orchestrators/TypecheckOrchestrator"
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
|
|
76
|
+
//#region Helpers
|
|
77
|
+
|
|
78
|
+
function createMockEngine() {
|
|
79
|
+
const engine = {
|
|
80
|
+
run: vi.fn().mockResolvedValue({
|
|
81
|
+
build: { success: true, errors: [], warnings: [], diagnostics: [] },
|
|
82
|
+
}),
|
|
83
|
+
startWatch: vi.fn(),
|
|
84
|
+
stop: vi.fn().mockResolvedValue(undefined),
|
|
85
|
+
};
|
|
86
|
+
mockEngines.push(engine);
|
|
87
|
+
return engine;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function setupDefaults(packages: Record<string, any> = {}) {
|
|
91
|
+
mocks.loadSdConfig.mockResolvedValue({ packages });
|
|
92
|
+
mocks.createBuildEngine.mockImplementation(() => createMockEngine() as any);
|
|
93
|
+
mocks.typecheckNonPackageFiles.mockReturnValue({
|
|
94
|
+
success: true, errorCount: 0, warningCount: 0, diagnostics: [],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
mocks.discoverWorkspacePackages.mockReturnValue(new Map<string, string>());
|
|
98
|
+
const pathMap = new Map<string, string>();
|
|
99
|
+
for (const name of Object.keys(packages)) {
|
|
100
|
+
pathMap.set(name, `packages/${name}`);
|
|
101
|
+
}
|
|
102
|
+
mocks.mergeTestsPackagesIntoConfig.mockReturnValue({
|
|
103
|
+
merged: packages,
|
|
104
|
+
pathMap,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
|
|
110
|
+
//#region Tests
|
|
111
|
+
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
vi.clearAllMocks();
|
|
114
|
+
mockEngines.length = 0;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("TypecheckOrchestrator", () => {
|
|
118
|
+
// Acceptance: Scenario "TypecheckOrchestrator 정상 실행"
|
|
119
|
+
it("produces correct TypecheckResult through init → start → shutdown lifecycle", async () => {
|
|
120
|
+
setupDefaults({
|
|
121
|
+
"core-common": { target: "neutral" },
|
|
122
|
+
"core-node": { target: "node" },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const orchestrator = new TypecheckOrchestrator({ targets: [], options: [] });
|
|
126
|
+
await orchestrator.initialize();
|
|
127
|
+
const result = await orchestrator.start();
|
|
128
|
+
await orchestrator.shutdown();
|
|
129
|
+
|
|
130
|
+
expect(result.success).toBe(true);
|
|
131
|
+
expect(result.errorCount).toBe(0);
|
|
132
|
+
expect(result.warningCount).toBe(0);
|
|
133
|
+
// neutral → 2 tasks (node + browser), node → 1 task = 3 engines
|
|
134
|
+
expect(mocks.createBuildEngine).toHaveBeenCalledTimes(3);
|
|
135
|
+
for (const engine of mockEngines) {
|
|
136
|
+
expect(engine.run).toHaveBeenCalledWith(
|
|
137
|
+
expect.objectContaining({ js: false, dts: false }),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Acceptance: Scenario "엔진 성공 시 리소스 정리"
|
|
143
|
+
it("calls engine.stop() after successful run", async () => {
|
|
144
|
+
setupDefaults({ "core-node": { target: "node" } });
|
|
145
|
+
|
|
146
|
+
const orchestrator = new TypecheckOrchestrator({ targets: [], options: [] });
|
|
147
|
+
await orchestrator.initialize();
|
|
148
|
+
await orchestrator.start();
|
|
149
|
+
await orchestrator.shutdown();
|
|
150
|
+
|
|
151
|
+
expect(mockEngines[0].stop).toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Acceptance: Scenario "엔진 실패 시 리소스 정리"
|
|
155
|
+
it("calls engine.stop() even when run fails", async () => {
|
|
156
|
+
setupDefaults({ "core-node": { target: "node" } });
|
|
157
|
+
mocks.createBuildEngine.mockImplementation(() => {
|
|
158
|
+
const engine = {
|
|
159
|
+
run: vi.fn().mockRejectedValue(new Error("build error")),
|
|
160
|
+
startWatch: vi.fn(),
|
|
161
|
+
stop: vi.fn().mockResolvedValue(undefined),
|
|
162
|
+
};
|
|
163
|
+
mockEngines.push(engine);
|
|
164
|
+
return engine as any;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const orchestrator = new TypecheckOrchestrator({ targets: [], options: [] });
|
|
168
|
+
await orchestrator.initialize();
|
|
169
|
+
const result = await orchestrator.start();
|
|
170
|
+
await orchestrator.shutdown();
|
|
171
|
+
|
|
172
|
+
expect(result.success).toBe(false);
|
|
173
|
+
expect(mockEngines[0].stop).toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// executeTypecheck 편의 함수 테스트는 tests/commands/typecheck.spec.ts에서
|
|
178
|
+
// 동일한 코드 경로를 통해 검증되므로 중복 삭제함
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# replaceDeps catch-all 에러 필터링 -- LLM 검증
|
|
2
|
+
|
|
3
|
+
## 검증 항목
|
|
4
|
+
|
|
5
|
+
- [x] catch 블록이 에러 코드를 확인하여 MODULE_NOT_FOUND/ERR_MODULE_NOT_FOUND만 무시: `sd-cli.ts:37-41` 확인 완료. `code` 프로퍼티를 체크하고 해당 코드가 아닌 경우 `console.warn`으로 경고 출력
|
|
6
|
+
- [x] 예상 외 에러(SyntaxError 등)에 경고 로그 출력: `console.warn("[sd-cli] replaceDeps 사전 설정 실패:", ...)` 확인
|
|
7
|
+
- [x] Phase 2로 정상 진행: catch 후 코드 흐름이 Phase 2(line 44~)로 계속됨 확인
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# lint.worker/lint-utils/check.ts import 경로 검증 -- LLM 검증
|
|
2
|
+
|
|
3
|
+
## 검증 항목
|
|
4
|
+
|
|
5
|
+
- [x] lint.worker.ts가 `../utils/lint-core`에서 import: `import { executeLint, type LintOptions, type LintResult } from "../utils/lint-core";` (line 2) 확인
|
|
6
|
+
- [x] lint-utils.ts가 `./lint-core`에서 import: `import type { LintOptions, LintResult } from "./lint-core";` (line 2) 확인
|
|
7
|
+
- [x] check.ts가 `../utils/lint-core`에서 import: `import { executeLint, type LintResult } from "../utils/lint-core";` (line 4) 확인
|
|
8
|
+
- [x] commands/lint.ts�� `../commands/lint` import가 없음: `import { executeLint, type LintOptions } from "../utils/lint-core";` (line 1)만 존재
|
|
9
|
+
- [x] lint-core.ts에 runLint가 없음: `executeLint`, `loadIgnorePatterns`, `LintOptions`, `LintResult`만 export
|
|
10
|
+
- [x] commands/lint.ts에 runLint만 존재: 13줄의 thin wrapper
|