@decohere/core 0.0.1-dev.126 → 0.0.1-dev.5
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/package.json +40 -15
- package/src/caching/cache-manager.ts +102 -0
- package/src/caching/cache.decorator.ts +56 -0
- package/src/caching/cache.ts +11 -0
- package/{es6/src/caching/index.js → src/caching/index.ts} +1 -1
- package/src/caching/invalidate-cache.decorator.ts +81 -0
- package/src/caching/memoize.decorator.ts +34 -0
- package/src/caching/node-cache-memoize.adapter.ts +47 -0
- package/src/cli/action.ts +77 -0
- package/src/cli/command.ts +173 -0
- package/src/cli/index.ts +30 -0
- package/src/cli/module.type.ts +48 -0
- package/src/cli/options.ts +81 -0
- package/src/collections/find-duplicates.ts +16 -0
- package/src/collections/merge-arrays.ts +9 -0
- package/src/collections/sum.ts +71 -0
- package/src/collections/unique-objects.ts +23 -0
- package/src/composition/index.ts +48 -0
- package/src/configuration/configuration.types.ts +13 -0
- package/src/diagnostics/index.ts +1 -0
- package/src/diagnostics/trace.ts +97 -0
- package/{es6/src/extensibility/decorator-factory-base.js → src/extensibility/decorator-factory-base.ts} +17 -9
- package/src/extensibility/decorator-factory.ts +44 -0
- package/src/extensibility/extension.ts +34 -0
- package/src/extensibility/mixwith.ts +42 -0
- package/src/extensibility/pipeline.ts +60 -0
- package/{types/src/extensibility/step.d.ts → src/extensibility/step.ts} +6 -4
- package/src/interop/docker/docker.ts +57 -0
- package/src/interop/errors/interop.error.ts +1 -0
- package/src/interop/git/git.ts +98 -0
- package/src/interop/git/git.types.ts +17 -0
- package/src/interop/index.ts +22 -0
- package/src/interop/node/index.ts +1 -0
- package/src/interop/node/node.ts +38 -0
- package/src/interop/npm/npm.ts +168 -0
- package/src/interop/npm/npm.types.ts +28 -0
- package/src/interop/pnpm/pnpm.ts +22 -0
- package/src/interop/pnpm/pnpm.types.ts +3 -0
- package/src/interop/shell/shell.argv.ts +31 -0
- package/src/interop/shell/shell.ts +100 -0
- package/src/interop/shell/shell.types.ts +9 -0
- package/src/interop/types/detect-package-manager.ts +45 -0
- package/src/interop/types/package-manager.ts +7 -0
- package/src/interop/yarn/yarn.ts +28 -0
- package/src/interop/yarn/yarn.types.ts +3 -0
- package/src/io/copy-file.ts +13 -0
- package/src/io/delete-file.ts +4 -0
- package/src/io/directory-exists.ts +9 -0
- package/src/io/file-exists.ts +10 -0
- package/src/io/find-directories.ts +13 -0
- package/src/io/find-file.ts +37 -0
- package/{es6/src/io/io.js → src/io/io.ts} +3 -0
- package/src/io/read-file.ts +25 -0
- package/src/io/remove-dir.ts +11 -0
- package/src/io/types/directory.type.ts +4 -0
- package/src/io/types/file.ts +34 -0
- package/src/io/url-to-path.ts +17 -0
- package/src/io/watch.ts +290 -0
- package/src/io/write-file.ts +21 -0
- package/src/objects/deep-merge.ts +131 -0
- package/{es6/src/reflection/index.js → src/reflection/index.ts} +1 -0
- package/src/reflection/types/reflection.types.ts +28 -0
- package/src/reflection/utilities.ts +92 -0
- package/src/semver/index.ts +1 -0
- package/src/semver/semver.ts +96 -0
- package/src/system/camel-case.ts +3 -0
- package/src/system/debounce.ts +13 -0
- package/src/system/decorator.ts +41 -0
- package/src/system/environment.ts +6 -0
- package/{types/src/system/function.types.d.ts → src/system/function.types.ts} +10 -2
- package/src/system/retry.decorator.ts +43 -0
- package/src/system/sleep.ts +6 -0
- package/src/system/type.utils.ts +5 -0
- package/src/testing/flush-promises.ts +7 -0
- package/src/testing/glob-with-memfs.ts +25 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/memfs.ts +28 -0
- package/src/testing/mock-logger.ts +42 -0
- package/src/testing/spy-on-constructor.ts +33 -0
- package/tests/caching/cache-manager.spec.ts +147 -0
- package/tests/caching/cache-test-class.ts +91 -0
- package/tests/caching/cache.decorator.spec.ts +259 -0
- package/tests/caching/invalidate-cache-test-class.ts +52 -0
- package/tests/caching/invalidate-cache.decorator.spec.ts +120 -0
- package/tests/caching/memoize-test-class.ts +99 -0
- package/tests/caching/memoize.decorator.spec.ts +203 -0
- package/tests/cli/command.model.spec.ts +91 -0
- package/tests/collections/merge-arrays.spec.ts +50 -0
- package/tests/collections/sum.spec.ts +86 -0
- package/tests/collections/unique-objects.spec.ts +233 -0
- package/tests/extensibility/extension.spec.ts +29 -0
- package/tests/extensibility/pipeline.spec.ts +379 -0
- package/tests/interop/detect-package-manager.spec.ts +70 -0
- package/tests/interop/npm.spec.ts +129 -0
- package/tests/interop/pnpm.spec.ts +30 -0
- package/tests/interop/shell.spec.ts +47 -0
- package/tests/interop/yarn.spec.ts +18 -0
- package/tests/io/find-file.spec.ts +188 -0
- package/tests/io/watch.real-fs.spec.ts +76 -0
- package/tests/io/watch.spec.ts +291 -0
- package/tests/mock-prototype.ts +12 -0
- package/tests/semver/semver.spec.ts +51 -0
- package/tests/system/decorator.spec.ts +250 -0
- package/tests/system/retry.decorator.spec.ts +57 -0
- package/LICENSE +0 -15
- package/commonjs/src/caching/cache-manager.js +0 -98
- package/commonjs/src/caching/cache.decorator.js +0 -69
- package/commonjs/src/caching/cache.js +0 -6
- package/commonjs/src/caching/index.js +0 -23
- package/commonjs/src/caching/invalidate-cache.decorator.js +0 -92
- package/commonjs/src/caching/map-cache.js +0 -39
- package/commonjs/src/caching/memoize.decorator.js +0 -36
- package/commonjs/src/collections/collections.js +0 -21
- package/commonjs/src/collections/find-duplicates.js +0 -35
- package/commonjs/src/collections/index.js +0 -52
- package/commonjs/src/collections/merge-arrays.js +0 -34
- package/commonjs/src/collections/sum.js +0 -63
- package/commonjs/src/collections/unique-objects.js +0 -39
- package/commonjs/src/composition/index.js +0 -4
- package/commonjs/src/configuration/configuration.types.js +0 -11
- package/commonjs/src/configuration/index.js +0 -18
- package/commonjs/src/extensibility/decorator-factory-base.js +0 -22
- package/commonjs/src/extensibility/decorator-factory.js +0 -31
- package/commonjs/src/extensibility/extension.js +0 -37
- package/commonjs/src/extensibility/index.js +0 -23
- package/commonjs/src/extensibility/mixwith.js +0 -36
- package/commonjs/src/extensibility/pipeline.js +0 -56
- package/commonjs/src/extensibility/step.js +0 -18
- package/commonjs/src/io/copy-file.js +0 -25
- package/commonjs/src/io/delete-file.js +0 -21
- package/commonjs/src/io/directory-exists.js +0 -24
- package/commonjs/src/io/file-exists.js +0 -19
- package/commonjs/src/io/index.js +0 -66
- package/commonjs/src/io/io.js +0 -25
- package/commonjs/src/io/read-file.js +0 -39
- package/commonjs/src/io/remove-dir.js +0 -22
- package/commonjs/src/io/types/directory.type.js +0 -4
- package/commonjs/src/io/types/file.js +0 -28
- package/commonjs/src/io/types/index.js +0 -19
- package/commonjs/src/io/url-to-path.js +0 -33
- package/commonjs/src/io/write-file.js +0 -30
- package/commonjs/src/objects/deep-merge.js +0 -67
- package/commonjs/src/objects/index.js +0 -11
- package/commonjs/src/reflection/index.js +0 -20
- package/commonjs/src/reflection/types/index.js +0 -18
- package/commonjs/src/reflection/types/reflection.types.js +0 -26
- package/commonjs/src/reflection/utilities.js +0 -118
- package/commonjs/src/system/camel-case.js +0 -13
- package/commonjs/src/system/debounce.js +0 -19
- package/commonjs/src/system/decorator.js +0 -46
- package/commonjs/src/system/environment.js +0 -23
- package/commonjs/src/system/function.types.js +0 -18
- package/commonjs/src/system/index.js +0 -24
- package/commonjs/src/system/retry.decorator.js +0 -40
- package/commonjs/src/system/sleep.js +0 -13
- package/commonjs/src/system/type.utils.js +0 -4
- package/es6/src/caching/cache-manager.js +0 -82
- package/es6/src/caching/cache.decorator.js +0 -51
- package/es6/src/caching/cache.js +0 -3
- package/es6/src/caching/invalidate-cache.decorator.js +0 -71
- package/es6/src/caching/map-cache.js +0 -35
- package/es6/src/caching/memoize.decorator.js +0 -27
- package/es6/src/collections/find-duplicates.js +0 -17
- package/es6/src/collections/merge-arrays.js +0 -16
- package/es6/src/collections/sum.js +0 -45
- package/es6/src/collections/unique-objects.js +0 -21
- package/es6/src/composition/index.js +0 -1
- package/es6/src/configuration/configuration.types.js +0 -1
- package/es6/src/extensibility/decorator-factory.js +0 -21
- package/es6/src/extensibility/extension.js +0 -27
- package/es6/src/extensibility/mixwith.js +0 -26
- package/es6/src/extensibility/pipeline.js +0 -46
- package/es6/src/extensibility/step.js +0 -16
- package/es6/src/io/copy-file.js +0 -10
- package/es6/src/io/delete-file.js +0 -6
- package/es6/src/io/directory-exists.js +0 -9
- package/es6/src/io/file-exists.js +0 -9
- package/es6/src/io/read-file.js +0 -13
- package/es6/src/io/remove-dir.js +0 -7
- package/es6/src/io/types/directory.type.js +0 -1
- package/es6/src/io/types/file.js +0 -18
- package/es6/src/io/url-to-path.js +0 -15
- package/es6/src/io/write-file.js +0 -15
- package/es6/src/objects/deep-merge.js +0 -57
- package/es6/src/reflection/types/reflection.types.js +0 -8
- package/es6/src/reflection/utilities.js +0 -74
- package/es6/src/system/camel-case.js +0 -3
- package/es6/src/system/debounce.js +0 -9
- package/es6/src/system/decorator.js +0 -30
- package/es6/src/system/environment.js +0 -5
- package/es6/src/system/function.types.js +0 -26
- package/es6/src/system/retry.decorator.js +0 -30
- package/es6/src/system/sleep.js +0 -3
- package/es6/src/system/type.utils.js +0 -1
- package/types/src/caching/cache-manager.d.ts +0 -59
- package/types/src/caching/cache.d.ts +0 -11
- package/types/src/caching/cache.decorator.d.ts +0 -18
- package/types/src/caching/index.d.ts +0 -6
- package/types/src/caching/invalidate-cache.decorator.d.ts +0 -17
- package/types/src/caching/map-cache.d.ts +0 -23
- package/types/src/caching/memoize.decorator.d.ts +0 -16
- package/types/src/collections/collections.d.ts +0 -4
- package/types/src/collections/find-duplicates.d.ts +0 -2
- package/types/src/collections/index.d.ts +0 -1
- package/types/src/collections/merge-arrays.d.ts +0 -2
- package/types/src/collections/sum.d.ts +0 -4
- package/types/src/collections/unique-objects.d.ts +0 -2
- package/types/src/composition/index.d.ts +0 -37
- package/types/src/configuration/configuration.types.d.ts +0 -10
- package/types/src/configuration/index.d.ts +0 -1
- package/types/src/extensibility/decorator-factory-base.d.ts +0 -39
- package/types/src/extensibility/decorator-factory.d.ts +0 -19
- package/types/src/extensibility/extension.d.ts +0 -9
- package/types/src/extensibility/index.d.ts +0 -6
- package/types/src/extensibility/mixwith.d.ts +0 -6
- package/types/src/extensibility/pipeline.d.ts +0 -12
- package/types/src/io/copy-file.d.ts +0 -4
- package/types/src/io/delete-file.d.ts +0 -1
- package/types/src/io/directory-exists.d.ts +0 -1
- package/types/src/io/file-exists.d.ts +0 -1
- package/types/src/io/index.d.ts +0 -2
- package/types/src/io/io.d.ts +0 -8
- package/types/src/io/read-file.d.ts +0 -4
- package/types/src/io/remove-dir.d.ts +0 -5
- package/types/src/io/types/directory.type.d.ts +0 -4
- package/types/src/io/types/file.d.ts +0 -20
- package/types/src/io/types/index.d.ts +0 -2
- package/types/src/io/url-to-path.d.ts +0 -2
- package/types/src/io/write-file.d.ts +0 -5
- package/types/src/objects/deep-merge.d.ts +0 -41
- package/types/src/objects/index.d.ts +0 -1
- package/types/src/reflection/index.d.ts +0 -3
- package/types/src/reflection/types/index.d.ts +0 -1
- package/types/src/reflection/types/reflection.types.d.ts +0 -21
- package/types/src/reflection/utilities.d.ts +0 -27
- package/types/src/system/camel-case.d.ts +0 -1
- package/types/src/system/debounce.d.ts +0 -1
- package/types/src/system/decorator.d.ts +0 -15
- package/types/src/system/environment.d.ts +0 -6
- package/types/src/system/index.d.ts +0 -7
- package/types/src/system/retry.decorator.d.ts +0 -6
- package/types/src/system/sleep.d.ts +0 -1
- package/types/src/system/type.utils.d.ts +0 -1
- /package/{es6/src/collections/collections.js → src/collections/collections.ts} +0 -0
- /package/{es6/src/collections/index.js → src/collections/index.ts} +0 -0
- /package/{es6/src/configuration/index.js → src/configuration/index.ts} +0 -0
- /package/{es6/src/extensibility/index.js → src/extensibility/index.ts} +0 -0
- /package/{es6/src/io/index.js → src/io/index.ts} +0 -0
- /package/{es6/src/io/types/index.js → src/io/types/index.ts} +0 -0
- /package/{es6/src/objects/index.js → src/objects/index.ts} +0 -0
- /package/{es6/src/reflection/types/index.js → src/reflection/types/index.ts} +0 -0
- /package/{es6/src/system/index.js → src/system/index.ts} +0 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { Pipeline, type PipelineStep } from "../../src/extensibility/pipeline.js";
|
|
3
|
+
|
|
4
|
+
type TestCtx = { log: string[] };
|
|
5
|
+
|
|
6
|
+
function makeStep(_name: string, sideEffect?: (ctx: TestCtx) => void): PipelineStep<TestCtx> {
|
|
7
|
+
return {
|
|
8
|
+
async run(ctx, next) {
|
|
9
|
+
sideEffect?.(ctx);
|
|
10
|
+
await next();
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("Pipeline", () => {
|
|
16
|
+
describe("step execution", () => {
|
|
17
|
+
it("runs a single step", async () => {
|
|
18
|
+
const ctx: TestCtx = { log: [] };
|
|
19
|
+
const pipeline = new Pipeline<TestCtx>(["A", makeStep("A", (c) => c.log.push("A"))]);
|
|
20
|
+
|
|
21
|
+
await pipeline.start(ctx);
|
|
22
|
+
|
|
23
|
+
expect(ctx.log).toEqual(["A"]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("runs multiple steps in order", async () => {
|
|
27
|
+
const ctx: TestCtx = { log: [] };
|
|
28
|
+
const pipeline = new Pipeline<TestCtx>(
|
|
29
|
+
["A", makeStep("A", (c) => c.log.push("A"))],
|
|
30
|
+
["B", makeStep("B", (c) => c.log.push("B"))],
|
|
31
|
+
["C", makeStep("C", (c) => c.log.push("C"))],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
await pipeline.start(ctx);
|
|
35
|
+
|
|
36
|
+
expect(ctx.log).toEqual(["A", "B", "C"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("completes with no steps without error", async () => {
|
|
40
|
+
const pipeline = new Pipeline<TestCtx>();
|
|
41
|
+
await expect(pipeline.start({ log: [] })).resolves.toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("passes the same context object to every step", async () => {
|
|
45
|
+
const ctx: TestCtx = { log: [] };
|
|
46
|
+
const received: TestCtx[] = [];
|
|
47
|
+
|
|
48
|
+
const capturingStep = (_name: string): PipelineStep<TestCtx> => ({
|
|
49
|
+
async run(c, next) {
|
|
50
|
+
received.push(c);
|
|
51
|
+
await next();
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const pipeline = new Pipeline<TestCtx>(
|
|
56
|
+
["A", capturingStep("A")],
|
|
57
|
+
["B", capturingStep("B")],
|
|
58
|
+
);
|
|
59
|
+
await pipeline.start(ctx);
|
|
60
|
+
|
|
61
|
+
expect(received).toHaveLength(2);
|
|
62
|
+
expect(received[0]).toBe(ctx);
|
|
63
|
+
expect(received[1]).toBe(ctx);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("next() control flow", () => {
|
|
68
|
+
it("stops at a step that does not call next()", async () => {
|
|
69
|
+
const ctx: TestCtx = { log: [] };
|
|
70
|
+
const blocker: PipelineStep<TestCtx> = {
|
|
71
|
+
async run(c, _next) {
|
|
72
|
+
c.log.push("blocker");
|
|
73
|
+
// intentionally does not call next()
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const pipeline = new Pipeline<TestCtx>(
|
|
77
|
+
["Blocker", blocker],
|
|
78
|
+
["After", makeStep("After", (c) => c.log.push("After"))],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await pipeline.start(ctx);
|
|
82
|
+
|
|
83
|
+
expect(ctx.log).toEqual(["blocker"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("allows a step to do work after calling next() (onion model)", async () => {
|
|
87
|
+
const ctx: TestCtx = { log: [] };
|
|
88
|
+
const wrapStep: PipelineStep<TestCtx> = {
|
|
89
|
+
async run(c, next) {
|
|
90
|
+
c.log.push("before");
|
|
91
|
+
await next();
|
|
92
|
+
c.log.push("after");
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
const pipeline = new Pipeline<TestCtx>(
|
|
96
|
+
["Wrap", wrapStep],
|
|
97
|
+
["Inner", makeStep("Inner", (c) => c.log.push("inner"))],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
await pipeline.start(ctx);
|
|
101
|
+
|
|
102
|
+
expect(ctx.log).toEqual(["before", "inner", "after"]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("step skip predicate (when)", () => {
|
|
107
|
+
it("runs a step when `when` returns true", async () => {
|
|
108
|
+
const ctx: TestCtx = { log: [] };
|
|
109
|
+
const runSpy = vi.fn();
|
|
110
|
+
const step = makeStep("A", (c) => {
|
|
111
|
+
c.log.push("A");
|
|
112
|
+
runSpy();
|
|
113
|
+
});
|
|
114
|
+
const pipeline = new Pipeline<TestCtx>(["A", step, () => true]);
|
|
115
|
+
|
|
116
|
+
await pipeline.start(ctx);
|
|
117
|
+
|
|
118
|
+
expect(runSpy).toHaveBeenCalledOnce();
|
|
119
|
+
expect(ctx.log).toEqual(["A"]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("skips a step when `when` returns false", async () => {
|
|
123
|
+
const ctx: TestCtx = { log: [] };
|
|
124
|
+
const runSpy = vi.fn();
|
|
125
|
+
const step = makeStep("A", () => runSpy());
|
|
126
|
+
const pipeline = new Pipeline<TestCtx>(["A", step, () => false]);
|
|
127
|
+
|
|
128
|
+
await pipeline.start(ctx);
|
|
129
|
+
|
|
130
|
+
expect(runSpy).not.toHaveBeenCalled();
|
|
131
|
+
expect(ctx.log).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("continues to downstream steps after a skipped step", async () => {
|
|
135
|
+
const ctx: TestCtx = { log: [] };
|
|
136
|
+
const pipeline = new Pipeline<TestCtx>(
|
|
137
|
+
["A", makeStep("A", (c) => c.log.push("A"))],
|
|
138
|
+
["B", makeStep("B", (c) => c.log.push("B")), () => false],
|
|
139
|
+
["C", makeStep("C", (c) => c.log.push("C"))],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
await pipeline.start(ctx);
|
|
143
|
+
|
|
144
|
+
expect(ctx.log).toEqual(["A", "C"]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("evaluates `when` against the live context at run time", async () => {
|
|
148
|
+
const ctx: TestCtx & { flag: boolean } = { log: [], flag: false };
|
|
149
|
+
const toggle: PipelineStep<typeof ctx> = {
|
|
150
|
+
async run(c, next) {
|
|
151
|
+
c.flag = true;
|
|
152
|
+
await next();
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
const conditional = makeStep("Conditional", (c) => c.log.push("conditional"));
|
|
156
|
+
const pipeline = new Pipeline<typeof ctx>(
|
|
157
|
+
["Toggle", toggle],
|
|
158
|
+
["Conditional", conditional, (c) => c.flag],
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
await pipeline.start(ctx);
|
|
162
|
+
|
|
163
|
+
expect(ctx.log).toEqual(["conditional"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("steps without a `when` predicate always run (backward compat)", async () => {
|
|
167
|
+
const ctx: TestCtx = { log: [] };
|
|
168
|
+
const pipeline = new Pipeline<TestCtx>(["A", makeStep("A", (c) => c.log.push("A"))]);
|
|
169
|
+
|
|
170
|
+
await pipeline.start(ctx);
|
|
171
|
+
|
|
172
|
+
expect(ctx.log).toEqual(["A"]);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("error handling", () => {
|
|
177
|
+
it("propagates errors thrown inside a step", async () => {
|
|
178
|
+
const thrower: PipelineStep<TestCtx> = {
|
|
179
|
+
async run(_c, _next) {
|
|
180
|
+
throw new Error("step failed");
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
const pipeline = new Pipeline<TestCtx>(["Thrower", thrower]);
|
|
184
|
+
|
|
185
|
+
await expect(pipeline.start({ log: [] })).rejects.toThrow("step failed");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("propagates errors from inner steps back through outer steps", async () => {
|
|
189
|
+
const ctx: TestCtx = { log: [] };
|
|
190
|
+
const outer: PipelineStep<TestCtx> = {
|
|
191
|
+
async run(c, next) {
|
|
192
|
+
try {
|
|
193
|
+
await next();
|
|
194
|
+
} catch {
|
|
195
|
+
c.log.push("caught-by-outer");
|
|
196
|
+
throw new Error("re-thrown");
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
const inner: PipelineStep<TestCtx> = {
|
|
201
|
+
async run(_c, _next) {
|
|
202
|
+
throw new Error("inner error");
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
const pipeline = new Pipeline<TestCtx>(["Outer", outer], ["Inner", inner]);
|
|
206
|
+
|
|
207
|
+
await expect(pipeline.start(ctx)).rejects.toThrow("re-thrown");
|
|
208
|
+
expect(ctx.log).toEqual(["caught-by-outer"]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("does not suppress errors even when step has a finally block", async () => {
|
|
212
|
+
const ctx: TestCtx = { log: [] };
|
|
213
|
+
const step: PipelineStep<TestCtx> = {
|
|
214
|
+
async run(c, next) {
|
|
215
|
+
try {
|
|
216
|
+
await next();
|
|
217
|
+
} finally {
|
|
218
|
+
c.log.push("finally");
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
const thrower: PipelineStep<TestCtx> = {
|
|
223
|
+
async run(_c, _next) {
|
|
224
|
+
throw new Error("boom");
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
const pipeline = new Pipeline<TestCtx>(["Step", step], ["Thrower", thrower]);
|
|
228
|
+
|
|
229
|
+
await expect(pipeline.start(ctx)).rejects.toThrow("boom");
|
|
230
|
+
expect(ctx.log).toEqual(["finally"]);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("async steps", () => {
|
|
235
|
+
it("awaits async steps before proceeding", async () => {
|
|
236
|
+
const ctx: TestCtx = { log: [] };
|
|
237
|
+
const asyncStep: PipelineStep<TestCtx> = {
|
|
238
|
+
async run(c, next) {
|
|
239
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
240
|
+
c.log.push("async");
|
|
241
|
+
await next();
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
const pipeline = new Pipeline<TestCtx>(
|
|
245
|
+
["Async", asyncStep],
|
|
246
|
+
["Sync", makeStep("Sync", (c) => c.log.push("sync"))],
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
await pipeline.start(ctx);
|
|
250
|
+
|
|
251
|
+
expect(ctx.log).toEqual(["async", "sync"]);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("dispose behavior", () => {
|
|
256
|
+
it("pipeline[Symbol.dispose]() calls disposers on steps that have them", async () => {
|
|
257
|
+
let disposedA = false;
|
|
258
|
+
let disposedB = false;
|
|
259
|
+
|
|
260
|
+
const stepA: PipelineStep<TestCtx> & Disposable = {
|
|
261
|
+
async run(ctx, next) {
|
|
262
|
+
ctx.log.push("A");
|
|
263
|
+
await next();
|
|
264
|
+
},
|
|
265
|
+
[Symbol.dispose]() {
|
|
266
|
+
disposedA = true;
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
const stepB: PipelineStep<TestCtx> = {
|
|
270
|
+
async run(ctx, next) {
|
|
271
|
+
ctx.log.push("B");
|
|
272
|
+
await next();
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
const stepC: PipelineStep<TestCtx> & Disposable = {
|
|
276
|
+
async run(ctx, next) {
|
|
277
|
+
ctx.log.push("C");
|
|
278
|
+
await next();
|
|
279
|
+
},
|
|
280
|
+
[Symbol.dispose]() {
|
|
281
|
+
disposedB = true;
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const pipeline = new Pipeline<TestCtx>(["A", stepA], ["B", stepB], ["C", stepC]);
|
|
286
|
+
await pipeline.start({ log: [] });
|
|
287
|
+
|
|
288
|
+
expect(disposedA).toBe(false);
|
|
289
|
+
expect(disposedB).toBe(false);
|
|
290
|
+
|
|
291
|
+
pipeline[Symbol.dispose]();
|
|
292
|
+
|
|
293
|
+
expect(disposedA).toBe(true);
|
|
294
|
+
expect(disposedB).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("does not call [Symbol.dispose]() when the step reference is popped from the stack", async () => {
|
|
298
|
+
let disposed = false;
|
|
299
|
+
|
|
300
|
+
class DisposableStep implements PipelineStep<TestCtx> {
|
|
301
|
+
public async run(ctx: TestCtx, next: () => Promise<void>) {
|
|
302
|
+
ctx.log.push("ran");
|
|
303
|
+
await next();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
public [Symbol.dispose]() {
|
|
307
|
+
disposed = true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const step = new DisposableStep();
|
|
312
|
+
const ctx: TestCtx = { log: [] };
|
|
313
|
+
const pipeline = new Pipeline<TestCtx>(["Disposable", step]);
|
|
314
|
+
|
|
315
|
+
await pipeline.start(ctx);
|
|
316
|
+
expect(ctx.log).toEqual(["ran"]);
|
|
317
|
+
|
|
318
|
+
// Simulate popping the step from the stack (dropping the Pipeline's reference)
|
|
319
|
+
(pipeline as any).stack.length = 0;
|
|
320
|
+
|
|
321
|
+
// [Symbol.dispose]() is NOT a GC finalizer — dropping the ref does not invoke it
|
|
322
|
+
expect(disposed).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("calls [Symbol.dispose]() only when explicitly invoked", async () => {
|
|
326
|
+
let disposed = false;
|
|
327
|
+
|
|
328
|
+
class DisposableStep implements PipelineStep<TestCtx> {
|
|
329
|
+
public async run(ctx: TestCtx, next: () => Promise<void>) {
|
|
330
|
+
ctx.log.push("ran");
|
|
331
|
+
await next();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
public [Symbol.dispose]() {
|
|
335
|
+
disposed = true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const step = new DisposableStep();
|
|
340
|
+
const pipeline = new Pipeline<TestCtx>(["Disposable", step]);
|
|
341
|
+
|
|
342
|
+
await pipeline.start({ log: [] });
|
|
343
|
+
|
|
344
|
+
(pipeline as any).stack.length = 0;
|
|
345
|
+
expect(disposed).toBe(false);
|
|
346
|
+
|
|
347
|
+
// Only an explicit call triggers the disposer
|
|
348
|
+
step[Symbol.dispose]();
|
|
349
|
+
expect(disposed).toBe(true);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("calls [Symbol.dispose]() automatically when the 'using' keyword is in scope", async () => {
|
|
353
|
+
let disposed = false;
|
|
354
|
+
|
|
355
|
+
class DisposableStep implements PipelineStep<TestCtx> {
|
|
356
|
+
public async run(ctx: TestCtx, next: () => Promise<void>) {
|
|
357
|
+
ctx.log.push("ran");
|
|
358
|
+
await next();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
public [Symbol.dispose]() {
|
|
362
|
+
disposed = true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const ctx: TestCtx = { log: [] };
|
|
367
|
+
|
|
368
|
+
{
|
|
369
|
+
using step = new DisposableStep();
|
|
370
|
+
const pipeline = new Pipeline<TestCtx>(["Disposable", step]);
|
|
371
|
+
await pipeline.start(ctx);
|
|
372
|
+
expect(disposed).toBe(false); // not yet — block hasn't exited
|
|
373
|
+
} // <-- [Symbol.dispose]() fires here automatically
|
|
374
|
+
|
|
375
|
+
expect(ctx.log).toEqual(["ran"]);
|
|
376
|
+
expect(disposed).toBe(true);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { Npm } from "../../src/interop/npm/npm.js";
|
|
3
|
+
import { Pnpm } from "../../src/interop/pnpm/pnpm.js";
|
|
4
|
+
import {
|
|
5
|
+
detectPackageManager,
|
|
6
|
+
packageManagerFromString,
|
|
7
|
+
} from "../../src/interop/types/detect-package-manager.js";
|
|
8
|
+
import { Yarn } from "../../src/interop/yarn/yarn.js";
|
|
9
|
+
|
|
10
|
+
vi.mock("node:fs", () => ({ existsSync: vi.fn().mockReturnValue(false) }));
|
|
11
|
+
|
|
12
|
+
const { existsSync } = await import("node:fs");
|
|
13
|
+
|
|
14
|
+
describe("detectPackageManager", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
delete process.env.npm_config_user_agent;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns Pnpm when pnpm-lock.yaml exists", () => {
|
|
24
|
+
vi.mocked(existsSync).mockImplementation((p) => String(p).endsWith("pnpm-lock.yaml"));
|
|
25
|
+
expect(detectPackageManager("/project")).toBeInstanceOf(Pnpm);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns Yarn when yarn.lock exists", () => {
|
|
29
|
+
vi.mocked(existsSync).mockImplementation((p) => String(p).endsWith("yarn.lock"));
|
|
30
|
+
expect(detectPackageManager("/project")).toBeInstanceOf(Yarn);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns Npm when package-lock.json exists", () => {
|
|
34
|
+
vi.mocked(existsSync).mockImplementation((p) => String(p).endsWith("package-lock.json"));
|
|
35
|
+
expect(detectPackageManager("/project")).toBeInstanceOf(Npm);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("prefers lockfile over user agent", () => {
|
|
39
|
+
vi.mocked(existsSync).mockImplementation((p) => String(p).endsWith("yarn.lock"));
|
|
40
|
+
process.env.npm_config_user_agent = "pnpm/9.0.0";
|
|
41
|
+
expect(detectPackageManager("/project")).toBeInstanceOf(Yarn);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("falls back to user agent when no lockfile", () => {
|
|
45
|
+
process.env.npm_config_user_agent = "pnpm/9.0.0 node/v22.0.0";
|
|
46
|
+
expect(detectPackageManager("/project")).toBeInstanceOf(Pnpm);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns undefined when no lockfile and no user agent", () => {
|
|
50
|
+
expect(detectPackageManager("/project")).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("packageManagerFromString", () => {
|
|
55
|
+
it("returns Yarn for 'yarn'", () => {
|
|
56
|
+
expect(packageManagerFromString("yarn")).toBeInstanceOf(Yarn);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns Npm for 'npm'", () => {
|
|
60
|
+
expect(packageManagerFromString("npm")).toBeInstanceOf(Npm);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("returns Pnpm for 'pnpm'", () => {
|
|
64
|
+
expect(packageManagerFromString("pnpm")).toBeInstanceOf(Pnpm);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("defaults to Pnpm for unknown string", () => {
|
|
68
|
+
expect(packageManagerFromString("bun")).toBeInstanceOf(Pnpm);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { vol } from "memfs";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { InteropError } from "../../src/interop/errors/interop.error.js";
|
|
5
|
+
|
|
6
|
+
const run = vi.hoisted(() => vi.fn());
|
|
7
|
+
|
|
8
|
+
vi.mock("../../src/interop/shell/shell.js", () => ({
|
|
9
|
+
Shell: class {
|
|
10
|
+
public run = run;
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
import { Npm } from "../../src/interop/npm/npm.js";
|
|
15
|
+
|
|
16
|
+
function seedPublishedPackage() {
|
|
17
|
+
vol.fromJSON({
|
|
18
|
+
"/pkg/package.json": JSON.stringify({
|
|
19
|
+
name: "@decohere/cli",
|
|
20
|
+
version: "0.0.1-dev.79",
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
mkdirSync("/tmp", { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("Npm", () => {
|
|
27
|
+
const originalToken = process.env.NODE_AUTH_TOKEN;
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
run.mockReset();
|
|
31
|
+
vol.reset();
|
|
32
|
+
if (originalToken == null) {
|
|
33
|
+
delete process.env.NODE_AUTH_TOKEN;
|
|
34
|
+
} else {
|
|
35
|
+
process.env.NODE_AUTH_TOKEN = originalToken;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("calls install with no extra flags", async () => {
|
|
40
|
+
await new Npm().install({ cwd: "/workspace" });
|
|
41
|
+
expect(run).toHaveBeenCalledWith({ cwd: "/workspace" }, "install");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("installs production dependencies as real folders, with no bin symlinks", async () => {
|
|
45
|
+
await new Npm().installProduction({ cwd: "/out" });
|
|
46
|
+
expect(run).toHaveBeenCalledWith(
|
|
47
|
+
{ cwd: "/out" },
|
|
48
|
+
"install",
|
|
49
|
+
"--omit=dev",
|
|
50
|
+
"--no-bin-links",
|
|
51
|
+
"--no-audit",
|
|
52
|
+
"--no-fund",
|
|
53
|
+
"--loglevel=error",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("publish dry-run still packs via npm publish --dry-run", async () => {
|
|
58
|
+
await new Npm().publish("dist/release/pkg", {
|
|
59
|
+
channel: "dev",
|
|
60
|
+
dryRun: true,
|
|
61
|
+
skipIfPublished: true,
|
|
62
|
+
});
|
|
63
|
+
expect(run).toHaveBeenCalledWith(
|
|
64
|
+
expect.objectContaining({ cwd: undefined, env: expect.any(Object) }),
|
|
65
|
+
"publish",
|
|
66
|
+
"dist/release/pkg",
|
|
67
|
+
"--tag",
|
|
68
|
+
"dev",
|
|
69
|
+
"--dry-run",
|
|
70
|
+
);
|
|
71
|
+
const env = run.mock.calls[0][0].env as NodeJS.ProcessEnv;
|
|
72
|
+
expect(env.NODE_AUTH_TOKEN).toBeUndefined();
|
|
73
|
+
expect(env.NPM_CONFIG_USERCONFIG).toBe("/dev/null");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("skipIfPublished uses ambient npm view (no token on argv)", async () => {
|
|
77
|
+
seedPublishedPackage();
|
|
78
|
+
process.env.NODE_AUTH_TOKEN = "read-token";
|
|
79
|
+
run.mockResolvedValueOnce({ message: "view", data: ["0.0.1-dev.79"] });
|
|
80
|
+
const result = await new Npm().publish("/pkg", { channel: "dev", skipIfPublished: true });
|
|
81
|
+
expect(result.message).toMatch(/Skipping publish/);
|
|
82
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(run.mock.calls[0][1]).toBe("view");
|
|
84
|
+
expect(run.mock.calls[0][2]).toBe("@decohere/cli@0.0.1-dev.79");
|
|
85
|
+
expect(run.mock.calls[0]).not.toContain("--userconfig");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("treats E403 already-published as skip and does not forward NODE_AUTH_TOKEN to npm publish", async () => {
|
|
89
|
+
seedPublishedPackage();
|
|
90
|
+
process.env.NODE_AUTH_TOKEN = "read-token";
|
|
91
|
+
run.mockRejectedValueOnce(new InteropError("404 Not Found"));
|
|
92
|
+
run.mockRejectedValueOnce(
|
|
93
|
+
new InteropError(
|
|
94
|
+
"npm error 403 Forbidden - You cannot publish over the previously published versions: 0.0.1-dev.79",
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
const result = await new Npm().publish("/pkg", { channel: "dev", skipIfPublished: true });
|
|
98
|
+
expect(result.message).toMatch(/Skipping publish/);
|
|
99
|
+
expect(run).toHaveBeenCalledTimes(2);
|
|
100
|
+
const publishCall = run.mock.calls[1];
|
|
101
|
+
expect(publishCall[1]).toBe("publish");
|
|
102
|
+
expect(publishCall[0].env.NODE_AUTH_TOKEN).toBeUndefined();
|
|
103
|
+
expect(publishCall[0].env.NPM_TOKEN).toBeUndefined();
|
|
104
|
+
expect(publishCall[0].env.NPM_CONFIG_USERCONFIG).toBe("/dev/null");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("publish passes otp via env, not argv", async () => {
|
|
108
|
+
await new Npm().publish("dist/release/pkg", {
|
|
109
|
+
channel: "beta",
|
|
110
|
+
dryRun: true,
|
|
111
|
+
otp: "123456",
|
|
112
|
+
});
|
|
113
|
+
expect(run).toHaveBeenCalledWith(
|
|
114
|
+
expect.objectContaining({ cwd: undefined, env: expect.any(Object) }),
|
|
115
|
+
"publish",
|
|
116
|
+
"dist/release/pkg",
|
|
117
|
+
"--tag",
|
|
118
|
+
"beta",
|
|
119
|
+
"--dry-run",
|
|
120
|
+
);
|
|
121
|
+
const env = run.mock.calls[0][0].env as NodeJS.ProcessEnv;
|
|
122
|
+
expect(env.NPM_CONFIG_OTP).toBe("123456");
|
|
123
|
+
expect(env.NODE_AUTH_TOKEN).toBeUndefined();
|
|
124
|
+
expect(env.NPM_CONFIG_USERCONFIG).toBe("/dev/null");
|
|
125
|
+
const argv = run.mock.calls[0]?.slice(1) ?? [];
|
|
126
|
+
expect(argv).not.toContain("--otp");
|
|
127
|
+
expect(argv).not.toContain("123456");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const run = vi.hoisted(() => vi.fn());
|
|
4
|
+
|
|
5
|
+
vi.mock("../../src/interop/shell/shell.js", () => ({
|
|
6
|
+
Shell: class {
|
|
7
|
+
public run = run;
|
|
8
|
+
},
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { Pnpm } from "../../src/interop/pnpm/pnpm.js";
|
|
12
|
+
|
|
13
|
+
describe("Pnpm", () => {
|
|
14
|
+
beforeEach(() => run.mockClear());
|
|
15
|
+
|
|
16
|
+
it("uses lockfile-only install flags when syncing lockfile", async () => {
|
|
17
|
+
await new Pnpm().syncLockFile({ cwd: "/workspace" });
|
|
18
|
+
expect(run).toHaveBeenCalledWith(
|
|
19
|
+
{ cwd: "/workspace" },
|
|
20
|
+
"install",
|
|
21
|
+
"--lockfile-only",
|
|
22
|
+
"--ignore-scripts",
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("calls install with no extra flags", async () => {
|
|
27
|
+
await new Pnpm().install({ cwd: "/workspace" });
|
|
28
|
+
expect(run).toHaveBeenCalledWith({ cwd: "/workspace" }, "install");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { Shell } from "../../src/interop/shell/shell.js";
|
|
5
|
+
|
|
6
|
+
vi.mock("node:child_process", () => ({
|
|
7
|
+
spawn: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
class TestShell extends Shell {
|
|
11
|
+
protected program = "missing-program";
|
|
12
|
+
|
|
13
|
+
public runCommand() {
|
|
14
|
+
return this.run({}, "arg");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("Shell", () => {
|
|
19
|
+
it("rejects when spawn emits an error", async () => {
|
|
20
|
+
const childProcess = new EventEmitter() as EventEmitter & {
|
|
21
|
+
stdout: EventEmitter;
|
|
22
|
+
};
|
|
23
|
+
childProcess.stdout = new EventEmitter();
|
|
24
|
+
|
|
25
|
+
vi.mocked(spawn).mockReturnValue(childProcess as never);
|
|
26
|
+
|
|
27
|
+
const shell = new TestShell();
|
|
28
|
+
const runPromise = shell.runCommand();
|
|
29
|
+
|
|
30
|
+
let settled = false;
|
|
31
|
+
let rejected = false;
|
|
32
|
+
runPromise
|
|
33
|
+
.then(() => {
|
|
34
|
+
settled = true;
|
|
35
|
+
})
|
|
36
|
+
.catch(() => {
|
|
37
|
+
settled = true;
|
|
38
|
+
rejected = true;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
childProcess.emit("error", new Error("spawn ENOENT"));
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
43
|
+
|
|
44
|
+
expect(settled).toBe(true);
|
|
45
|
+
expect(rejected).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const run = vi.hoisted(() => vi.fn());
|
|
4
|
+
|
|
5
|
+
vi.mock("../../src/interop/shell/shell.js", () => ({
|
|
6
|
+
Shell: class {
|
|
7
|
+
public run = run;
|
|
8
|
+
},
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { Yarn } from "../../src/interop/yarn/yarn.js";
|
|
12
|
+
|
|
13
|
+
describe("Yarn", () => {
|
|
14
|
+
it("calls install with no extra flags", async () => {
|
|
15
|
+
await new Yarn().install({ cwd: "/workspace" });
|
|
16
|
+
expect(run).toHaveBeenCalledWith({ cwd: "/workspace" }, "install");
|
|
17
|
+
});
|
|
18
|
+
});
|