@dsh-workbench/shared 1.1.14-beta.2 → 1.1.14
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/README.md +1 -1
- package/dist/api/dsh.d.ts +12 -0
- package/dist/components/SelectFieldWheel.d.ts +9 -0
- package/dist/components/SelectFieldWheel.js +35 -0
- package/dist/contract/unit-declaration.js +15 -1
- package/dist/contract/unit.d.ts +3 -0
- package/dist/host/__scratch-guard.d.ts +1 -0
- package/dist/host/__scratch-guard.js +1 -0
- package/dist/host/adapter-set.js +0 -1
- package/dist/host/changelog.js +2 -1
- package/dist/host/channel-plugin.d.ts +15 -0
- package/dist/host/channel-plugin.js +42 -0
- package/dist/host/command-name.d.ts +1 -0
- package/dist/host/command-name.js +14 -0
- package/dist/host/config-sync.d.ts +1 -9
- package/dist/host/config-sync.js +3 -78
- package/dist/host/env-policy.d.ts +75 -0
- package/dist/host/env-policy.js +161 -0
- package/dist/host/extension.js +2 -1
- package/dist/host/flow/run.js +2 -1
- package/dist/host/fs-write.d.ts +62 -0
- package/dist/host/fs-write.js +112 -0
- package/dist/host/host-kit.d.ts +6 -2
- package/dist/host/host-kit.js +3 -1
- package/dist/host/http.js +11 -5
- package/dist/host/installed-sources.js +4 -5
- package/dist/host/long-task.js +3 -3
- package/dist/host/opencli-runner.d.ts +26 -0
- package/dist/host/opencli-runner.js +50 -0
- package/dist/host/package-content.d.ts +2 -2
- package/dist/host/package-content.js +20 -6
- package/dist/host/package-route.d.ts +3 -3
- package/dist/host/package-route.js +7 -6
- package/dist/host/panel-state.js +2 -1
- package/dist/host/patch-blocks.d.ts +13 -0
- package/dist/host/patch-blocks.js +52 -0
- package/dist/host/patch.d.ts +3 -8
- package/dist/host/patch.js +5 -76
- package/dist/host/plugin-source.d.ts +33 -0
- package/dist/host/plugin-source.js +91 -0
- package/dist/host/port-types.d.ts +9 -9
- package/dist/host/profile-registries.js +2 -3
- package/dist/host/registry-declaration.d.ts +2 -2
- package/dist/host/registry-declaration.js +14 -5
- package/dist/host/release-probe.d.ts +4 -4
- package/dist/host/release-probe.js +39 -14
- package/dist/host/runner.d.ts +26 -0
- package/dist/host/runner.js +50 -0
- package/dist/host/runtime-links.d.ts +7 -0
- package/dist/host/runtime-links.js +25 -0
- package/dist/host/seam/denial.d.ts +4 -0
- package/dist/host/seam/denial.js +24 -0
- package/dist/host/seam/env.d.ts +15 -0
- package/dist/host/seam/env.js +17 -0
- package/dist/host/seam/exec.d.ts +84 -0
- package/dist/host/seam/exec.js +1 -0
- package/dist/host/seam/host-env.d.ts +2 -0
- package/dist/host/seam/host-env.js +18 -0
- package/dist/host/seam/intent.d.ts +66 -0
- package/dist/host/seam/intent.js +24 -0
- package/dist/host/seam/policy.d.ts +10 -0
- package/dist/host/seam/policy.js +85 -0
- package/dist/host/seam/process.d.ts +2 -0
- package/dist/host/seam/process.js +80 -0
- package/dist/host/seam/run.d.ts +44 -0
- package/dist/host/seam/run.js +421 -0
- package/dist/host/seam/service.d.ts +4 -0
- package/dist/host/seam/service.js +4 -0
- package/dist/host/seam/unit-seam.d.ts +14 -0
- package/dist/host/seam/unit-seam.js +43 -0
- package/dist/host/shell.d.ts +12 -0
- package/dist/host/shell.js +3 -1
- package/dist/host/time.d.ts +10 -0
- package/dist/host/time.js +14 -0
- package/dist/host/value-handoff.js +3 -2
- package/dist/host/workspace-store.js +2 -8
- package/dist/host.d.ts +11 -0
- package/dist/host.js +11 -0
- package/dist/i18n/en.js +3 -3
- package/dist/i18n/index.d.ts +4 -4
- package/dist/i18n/zh.d.ts +3 -3
- package/dist/i18n/zh.js +3 -3
- package/dist/lib/version.d.ts +4 -0
- package/dist/lib/version.js +21 -0
- package/dist/testing/fake-adapter-set.js +0 -1
- package/dist/testing/git.d.ts +3 -0
- package/dist/testing/git.js +36 -0
- package/dist/testing/host-fakes.d.ts +30 -0
- package/dist/testing/host-fakes.js +104 -0
- package/package.json +5 -2
- package/src/api/dsh.ts +8 -1
- package/src/contract/unit-declaration.ts +20 -1
- package/src/contract/unit.ts +3 -0
- package/src/host/__tests__/env-policy.test.ts +114 -0
- package/src/host/__tests__/fs-write.test.ts +96 -0
- package/src/host/__tests__/preflight-route.test.ts +29 -0
- package/src/host/__tests__/seam.test.ts +296 -0
- package/src/host/__tests__/shell.test.ts +24 -0
- package/src/host/__tests__/time.test.ts +20 -0
- package/src/host/adapter-set.ts +0 -1
- package/src/host/changelog.ts +2 -1
- package/src/host/config-sync.ts +3 -90
- package/src/host/env-policy.ts +192 -0
- package/src/host/extension.ts +2 -1
- package/src/host/flow/run.ts +2 -1
- package/src/host/fs-write.ts +137 -0
- package/src/host/host-kit.ts +7 -2
- package/src/host/http.ts +11 -5
- package/src/host/installed-sources.ts +4 -5
- package/src/host/long-task.ts +3 -3
- package/src/host/package-content.ts +23 -13
- package/src/host/package-route.ts +10 -9
- package/src/host/panel-state.ts +2 -1
- package/src/host/patch-blocks.ts +67 -0
- package/src/host/patch.ts +5 -89
- package/src/host/port-types.ts +9 -9
- package/src/host/profile-registries.ts +2 -3
- package/src/host/registry-declaration.ts +17 -12
- package/src/host/release-probe.ts +47 -34
- package/src/host/seam/denial.ts +26 -0
- package/src/host/seam/env.ts +23 -0
- package/src/host/seam/exec.ts +94 -0
- package/src/host/seam/host-env.ts +20 -0
- package/src/host/seam/intent.ts +101 -0
- package/src/host/seam/policy.ts +89 -0
- package/src/host/seam/process.ts +90 -0
- package/src/host/seam/run.ts +484 -0
- package/src/host/seam/service.ts +8 -0
- package/src/host/seam/unit-seam.ts +62 -0
- package/src/host/shell.ts +17 -1
- package/src/host/time.ts +16 -0
- package/src/host/value-handoff.ts +3 -2
- package/src/host/workspace-store.ts +2 -8
- package/src/host.ts +11 -0
- package/src/i18n/en.ts +3 -3
- package/src/i18n/zh.ts +3 -3
- package/src/testing/fake-adapter-set.ts +0 -1
- package/src/testing/host-fakes.ts +126 -0
- package/dist/lib/unit-plugin.d.ts +0 -28
- package/dist/lib/unit-plugin.js +0 -23
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join, delimiter } from 'node:path';
|
|
4
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
bootstrapEnvOf,
|
|
7
|
+
envTextOf,
|
|
8
|
+
inheritedEnvOf,
|
|
9
|
+
overlayEnvOf,
|
|
10
|
+
pathWithBinsOf,
|
|
11
|
+
productEnvOf,
|
|
12
|
+
promptlessGitEnvOf,
|
|
13
|
+
promptlessProcessEnvOf,
|
|
14
|
+
unitEnvOf,
|
|
15
|
+
} from '../env-policy.js';
|
|
16
|
+
|
|
17
|
+
const ROOT = mkdtempSync(join(tmpdir(), 'dsh-wb-env-policy-'));
|
|
18
|
+
const REAL_HOME = join(ROOT, 'real-home');
|
|
19
|
+
const RUNTIME = join(ROOT, 'workbench');
|
|
20
|
+
const DSH_HOME = process.env.DSH_HOME;
|
|
21
|
+
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
mkdirSync(REAL_HOME, { recursive: true });
|
|
24
|
+
writeFileSync(join(REAL_HOME, '.npmrc'), 'registry=https://example.test\n');
|
|
25
|
+
writeFileSync(join(REAL_HOME, '.gitconfig'), '[user]\n');
|
|
26
|
+
process.env.DSH_HOME = join(ROOT, 'dsh');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
if (DSH_HOME === undefined) delete process.env.DSH_HOME;
|
|
31
|
+
else process.env.DSH_HOME = DSH_HOME;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
rmSync(ROOT, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('受控 env 入口', () => {
|
|
39
|
+
it('product:HOME / 缓存全钉在运行时根,真 npmrc/gitconfig 只读指过去', () => {
|
|
40
|
+
const env = productEnvOf({ runtimeRoot: RUNTIME, realHome: REAL_HOME });
|
|
41
|
+
expect(env['HOME']).toBe(join(RUNTIME, 'home'));
|
|
42
|
+
expect(env['DSH_WORKBENCH_HOME']).toBe(RUNTIME);
|
|
43
|
+
expect(env['DSH_HOME']).toBe(join(ROOT, 'dsh'));
|
|
44
|
+
expect(env['npm_config_cache']).toBe(join(RUNTIME, 'npm-cache'));
|
|
45
|
+
expect(env['NPM_CONFIG_CACHE']).toBe(join(RUNTIME, 'npm-cache'));
|
|
46
|
+
expect(env['npm_config_store_dir']).toBe(join(RUNTIME, 'pnpm-store'));
|
|
47
|
+
expect(env['NPM_CONFIG_USERCONFIG']).toBe(join(REAL_HOME, '.npmrc'));
|
|
48
|
+
expect(env['GIT_CONFIG_GLOBAL']).toBe(join(REAL_HOME, '.gitconfig'));
|
|
49
|
+
expect(env['NO_UPDATE_NOTIFIER']).toBe('1');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('product:没有真配置时不指过去', () => {
|
|
53
|
+
const emptyHome = join(ROOT, 'empty-home');
|
|
54
|
+
mkdirSync(emptyHome, { recursive: true });
|
|
55
|
+
const env = productEnvOf({ runtimeRoot: RUNTIME, realHome: emptyHome });
|
|
56
|
+
expect(env['NPM_CONFIG_USERCONFIG']).toBeUndefined();
|
|
57
|
+
expect(env['GIT_CONFIG_GLOBAL']).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('unit:HOME 与 npm/pnpm 目录钉到单元数据目录', () => {
|
|
61
|
+
const unitRoot = join(RUNTIME, 'units', 'demo');
|
|
62
|
+
const env = unitEnvOf({ runtimeRoot: RUNTIME, unitRoot, realHome: REAL_HOME });
|
|
63
|
+
expect(env['HOME']).toBe(join(unitRoot, 'home'));
|
|
64
|
+
expect(env['npm_config_cache']).toBe(join(unitRoot, 'npm-cache'));
|
|
65
|
+
expect(env['npm_config_store_dir']).toBe(join(unitRoot, 'pnpm-store'));
|
|
66
|
+
expect(env['DSH_WORKBENCH_HOME']).toBe(RUNTIME);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('bootstrap:继承父环境、HOME/缓存钉住、PATH 可替换、pwsh 加 USERPROFILE', () => {
|
|
70
|
+
const parent = { PATH: '/usr/bin', KEEP: 'yes' };
|
|
71
|
+
const env = bootstrapEnvOf({
|
|
72
|
+
root: join(RUNTIME, 'opencli'),
|
|
73
|
+
realHome: REAL_HOME,
|
|
74
|
+
shellKind: 'posix',
|
|
75
|
+
parent,
|
|
76
|
+
path: '/desktop/bin' + delimiter + '/usr/bin',
|
|
77
|
+
});
|
|
78
|
+
expect(env['KEEP']).toBe('yes');
|
|
79
|
+
expect(env['PATH']).toBe('/desktop/bin' + delimiter + '/usr/bin');
|
|
80
|
+
expect(env['HOME']).toBe(join(RUNTIME, 'opencli', 'home'));
|
|
81
|
+
expect(env['npm_config_cache']).toBe(join(RUNTIME, 'opencli', 'npm-cache'));
|
|
82
|
+
expect(env['NPM_CONFIG_USERCONFIG']).toBe(join(REAL_HOME, '.npmrc'));
|
|
83
|
+
expect(env['NO_COLOR']).toBe('1');
|
|
84
|
+
expect(env['TERM']).toBe('dumb');
|
|
85
|
+
expect(env['USERPROFILE']).toBeUndefined();
|
|
86
|
+
const pwsh = bootstrapEnvOf({ root: join(RUNTIME, 'opencli'), realHome: REAL_HOME, shellKind: 'pwsh', parent });
|
|
87
|
+
expect(pwsh['USERPROFILE']).toBe(join(RUNTIME, 'opencli', 'home'));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('promptless:posix 关 askpass 指向 false,win32 删掉父进程的 askpass', () => {
|
|
91
|
+
const posix = promptlessGitEnvOf('darwin');
|
|
92
|
+
expect(posix['GIT_TERMINAL_PROMPT']).toBe('0');
|
|
93
|
+
expect(posix['GIT_ASKPASS']).toBe('/usr/bin/false');
|
|
94
|
+
const win = promptlessGitEnvOf('win32');
|
|
95
|
+
expect(win).not.toHaveProperty('GIT_ASKPASS');
|
|
96
|
+
const parent = { GIT_ASKPASS: 'C:/evil.exe', KEEP: '1' };
|
|
97
|
+
const merged = promptlessProcessEnvOf(parent, 'win32');
|
|
98
|
+
expect(merged['GIT_ASKPASS']).toBeUndefined();
|
|
99
|
+
expect(merged['KEEP']).toBe('1');
|
|
100
|
+
expect(merged['GIT_TERMINAL_PROMPT']).toBe('0');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('叠层:undefined 删键,envTextOf 丢掉非字符串', () => {
|
|
104
|
+
const merged = overlayEnvOf({ A: '1', B: '2' }, { B: undefined, C: '3' });
|
|
105
|
+
expect(merged).toEqual({ A: '1', C: '3' });
|
|
106
|
+
expect(inheritedEnvOf({ A: '1' })).toEqual({ A: '1' });
|
|
107
|
+
expect(envTextOf({ A: '1', B: undefined })).toEqual({ A: '1' });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('PATH 前插去重', () => {
|
|
111
|
+
expect(pathWithBinsOf(['/a', '/b'].join(delimiter), ['/a', '/c'])).toBe(['/a', '/c', '/b'].join(delimiter));
|
|
112
|
+
expect(pathWithBinsOf(undefined, ['/x'])).toBe('/x');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { chmodSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterAll, describe, expect, it } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
ensureDir,
|
|
7
|
+
makeExecutable,
|
|
8
|
+
writeBytesFile,
|
|
9
|
+
writeBytesFileAsync,
|
|
10
|
+
writeJsonFile,
|
|
11
|
+
writeJsonFileAsync,
|
|
12
|
+
writeTextFile,
|
|
13
|
+
writeTextFileAsync,
|
|
14
|
+
} from '../fs-write.js';
|
|
15
|
+
|
|
16
|
+
const ROOT = mkdtempSync(join(tmpdir(), 'dsh-wb-fs-write-'));
|
|
17
|
+
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
rmSync(ROOT, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('宿主文件写入口', () => {
|
|
23
|
+
it('写文本:建父目录、原子落盘、内容正确', () => {
|
|
24
|
+
const file = join(ROOT, 'a', 'b', 'note.txt');
|
|
25
|
+
writeTextFile(file, 'hello');
|
|
26
|
+
expect(readFileSync(file, 'utf8')).toBe('hello');
|
|
27
|
+
expect(readdirSync(join(ROOT, 'a', 'b'))).toEqual(['note.txt']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('mode 默认保留已有文件的权限位', () => {
|
|
31
|
+
const file = join(ROOT, 'secret.txt');
|
|
32
|
+
writeFileSync(file, 'old', { mode: 0o600 });
|
|
33
|
+
writeTextFile(file, 'new');
|
|
34
|
+
expect(statSync(file).mode & 0o777).toBe(0o600);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('显式 mode 生效,可执行文件 0755', () => {
|
|
38
|
+
const secret = join(ROOT, 'explicit.txt');
|
|
39
|
+
writeTextFile(secret, 's', { mode: 0o600 });
|
|
40
|
+
expect(statSync(secret).mode & 0o777).toBe(0o600);
|
|
41
|
+
const script = join(ROOT, 'bin', 'run.sh');
|
|
42
|
+
writeTextFile(script, '#!/bin/sh\n', { mode: 0o755 });
|
|
43
|
+
expect(statSync(script).mode & 0o777).toBe(0o755);
|
|
44
|
+
const plain = join(ROOT, 'plain.sh');
|
|
45
|
+
writeTextFile(plain, '#!/bin/sh\n');
|
|
46
|
+
makeExecutable(plain);
|
|
47
|
+
expect(statSync(plain).mode & 0o777).toBe(0o755);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('JSON:两空格缩进 + 末尾换行', () => {
|
|
51
|
+
const file = join(ROOT, 'state.json');
|
|
52
|
+
writeJsonFile(file, { a: 1, list: [2] });
|
|
53
|
+
expect(readFileSync(file, 'utf8')).toBe('{\n "a": 1,\n "list": [\n 2\n ]\n}\n');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('atomic:false 直接写,不建父目录', () => {
|
|
57
|
+
const file = join(ROOT, 'direct.txt');
|
|
58
|
+
writeTextFile(file, 'x', { atomic: false });
|
|
59
|
+
expect(readFileSync(file, 'utf8')).toBe('x');
|
|
60
|
+
expect(() => writeTextFile(join(ROOT, 'missing', 'deep.txt'), 'x', { atomic: false, mkdir: false })).toThrow();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('二进制写:字节原样落盘', () => {
|
|
64
|
+
const file = join(ROOT, 'img.bin');
|
|
65
|
+
writeBytesFile(file, new Uint8Array([0, 255, 7]));
|
|
66
|
+
expect([...readFileSync(file)]).toEqual([0, 255, 7]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('异步版语义一致', async () => {
|
|
70
|
+
const file = join(ROOT, 'async.json');
|
|
71
|
+
await writeJsonFileAsync(file, { ok: true });
|
|
72
|
+
expect(JSON.parse(readFileSync(file, 'utf8'))).toEqual({ ok: true });
|
|
73
|
+
const text = join(ROOT, 'async.txt');
|
|
74
|
+
await writeTextFileAsync(text, 'a');
|
|
75
|
+
expect(readFileSync(text, 'utf8')).toBe('a');
|
|
76
|
+
const bin = join(ROOT, 'async.bin');
|
|
77
|
+
await writeBytesFileAsync(bin, new Uint8Array([1, 2]));
|
|
78
|
+
expect([...readFileSync(bin)]).toEqual([1, 2]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('并发写同一文件:结果完整,不残留临时文件', async () => {
|
|
82
|
+
const file = join(ROOT, 'race.txt');
|
|
83
|
+
await Promise.all(Array.from({ length: 12 }, (_, i) => writeTextFileAsync(file, `v${String(i)}`)));
|
|
84
|
+
expect(readFileSync(file, 'utf8')).toMatch(/^v\d+$/);
|
|
85
|
+
expect(readdirSync(ROOT).filter((name) => name.startsWith('race.txt'))).toEqual(['race.txt']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('ensureDir 幂等', () => {
|
|
89
|
+
const dir = join(ROOT, 'x', 'y');
|
|
90
|
+
ensureDir(dir);
|
|
91
|
+
ensureDir(dir);
|
|
92
|
+
writeFileSync(join(dir, 'f'), '', 'utf8');
|
|
93
|
+
chmodSync(join(dir, 'f'), 0o644);
|
|
94
|
+
expect(readdirSync(dir)).toEqual(['f']);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -30,4 +30,33 @@ describe('preflightHandler(体检那一拍带什么参数)', () => {
|
|
|
30
30
|
|
|
31
31
|
expect(handler(new URLSearchParams(''))).toEqual({ ok: true, ...SNAPSHOT });
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
it('★ 先回上一轮的结果再起新一轮:run 一上来就把快照重置成 checking,也不许把它顶掉', () => {
|
|
35
|
+
let current = { phase: 'failed', steps: [], at: 1 };
|
|
36
|
+
const run = vi.fn(async () => {
|
|
37
|
+
current = { phase: 'checking', steps: [], at: 2 };
|
|
38
|
+
return current;
|
|
39
|
+
});
|
|
40
|
+
const handler = preflightHandler({ run, snapshot: () => current });
|
|
41
|
+
|
|
42
|
+
const reply = handler(new URLSearchParams(''));
|
|
43
|
+
|
|
44
|
+
expect(reply).toEqual({ ok: true, phase: 'failed', steps: [], at: 1 });
|
|
45
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
46
|
+
expect(current.phase).toBe('checking');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('★ 重试(hard)回的是新一轮的进度:别把上一轮的 failed 又顶回画面', () => {
|
|
50
|
+
let current = { phase: 'failed', steps: [], at: 1 };
|
|
51
|
+
const run = vi.fn(async () => {
|
|
52
|
+
current = { phase: 'checking', steps: [], at: 2 };
|
|
53
|
+
return current;
|
|
54
|
+
});
|
|
55
|
+
const handler = preflightHandler({ run, snapshot: () => current });
|
|
56
|
+
|
|
57
|
+
const reply = handler(new URLSearchParams('hard=1'));
|
|
58
|
+
|
|
59
|
+
expect(reply).toEqual({ ok: true, phase: 'checking', steps: [], at: 2 });
|
|
60
|
+
expect(run).toHaveBeenCalledWith({ hard: true });
|
|
61
|
+
});
|
|
33
62
|
});
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterAll, describe, expect, it } from 'vitest';
|
|
5
|
+
import { SeamError } from '../seam/intent.js';
|
|
6
|
+
import { createSeam } from '../seam/run.js';
|
|
7
|
+
import { createUnitSeam, unitDataRoot } from '../seam/unit-seam.js';
|
|
8
|
+
import { createFakeProcesses, createFakeShell, createFakeLogger } from '../../testing/host-fakes.js';
|
|
9
|
+
|
|
10
|
+
const ROOT = join(tmpdir(), 'dsh-wb-seam-root');
|
|
11
|
+
const PROJECT = join(tmpdir(), 'dsh-wb-seam-project');
|
|
12
|
+
const SCRATCH = mkdtempSync(join(tmpdir(), 'dsh-wb-seam-'));
|
|
13
|
+
const REAL_HOME = join(SCRATCH, 'home');
|
|
14
|
+
|
|
15
|
+
afterAll(() => {
|
|
16
|
+
rmSync(SCRATCH, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function policyOf(call: { sandboxPolicy?: unknown } | undefined): unknown {
|
|
20
|
+
return call?.sandboxPolicy;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('createSeam', () => {
|
|
24
|
+
it('产品域:固定根 + 注入产品环境', async () => {
|
|
25
|
+
const fake = createFakeShell([{}]);
|
|
26
|
+
const seam = createSeam({ shell: fake.shell, runtimeRoot: ROOT, realHome: REAL_HOME });
|
|
27
|
+
const outcome = await seam.run({ command: 'npm view left-pad version', intent: { kind: 'product' } });
|
|
28
|
+
expect(outcome.exitCode).toBe(0);
|
|
29
|
+
expect(policyOf(fake.calls[0])).toEqual({ mode: 'workspace-write', workspaceRoot: ROOT });
|
|
30
|
+
const env = fake.calls[0]?.env;
|
|
31
|
+
expect(env?.['HOME']).toBe(join(ROOT, 'home'));
|
|
32
|
+
expect(env?.['npm_config_cache']).toBe(join(ROOT, 'npm-cache'));
|
|
33
|
+
expect(env?.['DSH_WORKBENCH_HOME']).toBe(ROOT);
|
|
34
|
+
expect(env?.['npm_config_state_dir']).toBe(join(ROOT, 'pnpm-state'));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('产品域不跟会话只读,只跟部署上限', async () => {
|
|
38
|
+
const fake = createFakeShell([{}]);
|
|
39
|
+
const seam = createSeam({
|
|
40
|
+
shell: fake.shell,
|
|
41
|
+
runtimeRoot: ROOT,
|
|
42
|
+
ceiling: 'workspace-write',
|
|
43
|
+
grantOf: () => ({ mode: 'read-only', workspaceRoot: PROJECT }),
|
|
44
|
+
});
|
|
45
|
+
await seam.run({ command: 'npm view x version', intent: { kind: 'product' } });
|
|
46
|
+
expect(policyOf(fake.calls[0])).toEqual({ mode: 'workspace-write', workspaceRoot: ROOT });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('工作区域:取会话解析结果;上限更宽时按会话', async () => {
|
|
50
|
+
const fake = createFakeShell([{}]);
|
|
51
|
+
const seam = createSeam({
|
|
52
|
+
shell: fake.shell,
|
|
53
|
+
runtimeRoot: ROOT,
|
|
54
|
+
ceiling: 'danger-full-access',
|
|
55
|
+
grantOf: () => ({ mode: 'danger-full-access', workspaceRoot: PROJECT }),
|
|
56
|
+
});
|
|
57
|
+
await seam.run({ command: 'pnpm install', intent: { kind: 'workspace' }, session: {} });
|
|
58
|
+
expect(policyOf(fake.calls[0])).toEqual({ mode: 'danger-full-access', workspaceRoot: PROJECT });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('会话只读:起进程前就拒绝,不起进程', async () => {
|
|
62
|
+
const fake = createFakeShell([{}]);
|
|
63
|
+
const seam = createSeam({
|
|
64
|
+
shell: fake.shell,
|
|
65
|
+
runtimeRoot: ROOT,
|
|
66
|
+
grantOf: () => ({ mode: 'read-only', workspaceRoot: PROJECT }),
|
|
67
|
+
});
|
|
68
|
+
await expect(seam.run({ command: 'pnpm install', intent: { kind: 'workspace' }, session: {} })).rejects.toMatchObject({
|
|
69
|
+
code: 'read-only',
|
|
70
|
+
});
|
|
71
|
+
expect(fake.calls).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('没有会话却要写工作区:起进程前报 workspace-unknown', async () => {
|
|
75
|
+
const fake = createFakeShell([{}]);
|
|
76
|
+
const seam = createSeam({ shell: fake.shell, runtimeRoot: ROOT });
|
|
77
|
+
await expect(seam.run({ command: 'pnpm install', intent: { kind: 'workspace' } })).rejects.toMatchObject({
|
|
78
|
+
code: 'workspace-unknown',
|
|
79
|
+
});
|
|
80
|
+
expect(fake.calls).toHaveLength(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('外部路径:同一目录可以,跨目录拒绝', async () => {
|
|
84
|
+
const fake = createFakeShell([{}]);
|
|
85
|
+
const seam = createSeam({ shell: fake.shell, runtimeRoot: ROOT });
|
|
86
|
+
await seam.run({ command: 'png', intent: { kind: 'paths', paths: [join(SCRATCH, 'a.png'), join(SCRATCH, 'b.png')] } });
|
|
87
|
+
expect(policyOf(fake.calls[0])).toEqual({ mode: 'workspace-write', workspaceRoot: SCRATCH });
|
|
88
|
+
await expect(
|
|
89
|
+
seam.run({ command: 'png', intent: { kind: 'paths', paths: [join(SCRATCH, 'a.png'), '/etc/hosts'] } }),
|
|
90
|
+
).rejects.toMatchObject({ code: 'paths-span-trees' });
|
|
91
|
+
expect(fake.calls).toHaveLength(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('安全重试:产品域被拒后按产品域重跑一次并标记 healed', async () => {
|
|
95
|
+
const fake = createFakeShell([
|
|
96
|
+
{
|
|
97
|
+
exitCode: 1,
|
|
98
|
+
stderr: "npm error code EPERM\nnpm error path " + join(ROOT, 'npm-cache', '_cacache') + "\n",
|
|
99
|
+
sandbox: { mode: 'workspace-write', denied: true },
|
|
100
|
+
},
|
|
101
|
+
{},
|
|
102
|
+
]);
|
|
103
|
+
const seam = createSeam({
|
|
104
|
+
shell: fake.shell,
|
|
105
|
+
runtimeRoot: ROOT,
|
|
106
|
+
grantOf: () => ({ mode: 'workspace-write', workspaceRoot: PROJECT }),
|
|
107
|
+
});
|
|
108
|
+
const outcome = await seam.run({
|
|
109
|
+
command: 'npm publish',
|
|
110
|
+
intent: { kind: 'workspace' },
|
|
111
|
+
session: {},
|
|
112
|
+
retry: 'safe',
|
|
113
|
+
});
|
|
114
|
+
expect(fake.calls).toHaveLength(2);
|
|
115
|
+
expect(policyOf(fake.calls[1])).toEqual({ mode: 'workspace-write', workspaceRoot: ROOT });
|
|
116
|
+
expect(outcome.denial?.healed).toBe(true);
|
|
117
|
+
expect(outcome.denial?.scope).toBe('product');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('写命令默认不自动重试:denial 直接给类型化错误', async () => {
|
|
121
|
+
const fake = createFakeShell([
|
|
122
|
+
{
|
|
123
|
+
exitCode: 1,
|
|
124
|
+
stderr: "EPERM: operation not permitted, mkdir '" + join(ROOT, 'npm-cache') + "'\n",
|
|
125
|
+
sandbox: { mode: 'workspace-write', denied: true },
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
const seam = createSeam({
|
|
129
|
+
shell: fake.shell,
|
|
130
|
+
runtimeRoot: ROOT,
|
|
131
|
+
grantOf: () => ({ mode: 'workspace-write', workspaceRoot: PROJECT }),
|
|
132
|
+
});
|
|
133
|
+
await expect(
|
|
134
|
+
seam.run({ command: 'git commit', intent: { kind: 'workspace' }, session: {} }),
|
|
135
|
+
).rejects.toMatchObject({ code: 'sandbox-denied' });
|
|
136
|
+
expect(fake.calls).toHaveLength(1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('外部路径被拒:bestEffort 返回带 denial 的结果,否则抛类型化错误', async () => {
|
|
140
|
+
const external = join(tmpdir(), 'dsh-wb-seam-external', 'shot.png');
|
|
141
|
+
const denialReply = {
|
|
142
|
+
exitCode: 1,
|
|
143
|
+
stderr: "EPERM: operation not permitted, open '" + external + "'\n",
|
|
144
|
+
sandbox: { mode: 'workspace-write', denied: true },
|
|
145
|
+
};
|
|
146
|
+
const best = createFakeShell([denialReply]);
|
|
147
|
+
const seamBest = createSeam({ shell: best.shell, runtimeRoot: ROOT, logger: createFakeLogger().logger });
|
|
148
|
+
const outcome = await seamBest.run({ command: 'png', intent: { kind: 'product' }, bestEffort: true });
|
|
149
|
+
expect(outcome.denial?.scope).toBe('external');
|
|
150
|
+
expect(outcome.denial?.path).toBe(external);
|
|
151
|
+
|
|
152
|
+
const strict = createFakeShell([denialReply]);
|
|
153
|
+
const seamStrict = createSeam({ shell: strict.shell, runtimeRoot: ROOT });
|
|
154
|
+
await expect(seamStrict.run({ command: 'png', intent: { kind: 'product' } })).rejects.toBeInstanceOf(SeamError);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('每次调用都记账', async () => {
|
|
158
|
+
const fake = createFakeShell([{}, {}]);
|
|
159
|
+
const seam = createSeam({ shell: fake.shell, runtimeRoot: ROOT });
|
|
160
|
+
await seam.run({ command: 'git status', intent: { kind: 'none' } });
|
|
161
|
+
await seam.run({ command: 'npm view x version', intent: { kind: 'product' } });
|
|
162
|
+
expect(seam.facts()).toHaveLength(2);
|
|
163
|
+
expect(seam.facts()[0]).toMatchObject({ command: 'git status', intent: 'none', mode: 'workspace-write' });
|
|
164
|
+
expect(seam.facts()[1]).toMatchObject({ command: 'npm view x version', intent: 'product' });
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('createUnitSeam', () => {
|
|
169
|
+
it('unit 域:根在产品域内,env 落在模块目录', async () => {
|
|
170
|
+
const fake = createFakeShell([{}]);
|
|
171
|
+
const seam = createSeam({ shell: fake.shell, runtimeRoot: ROOT, realHome: REAL_HOME });
|
|
172
|
+
const unit = createUnitSeam({ seam, unit: 'demo', runtimeRoot: ROOT, realHome: REAL_HOME });
|
|
173
|
+
await unit.run({ command: 'echo hi', scope: 'unit' });
|
|
174
|
+
expect(policyOf(fake.calls[0])).toEqual({ mode: 'workspace-write', workspaceRoot: ROOT });
|
|
175
|
+
const env = fake.calls[0]?.env;
|
|
176
|
+
expect(env?.['HOME']).toBe(join(unitDataRoot(ROOT, 'demo'), 'home'));
|
|
177
|
+
expect(env?.['npm_config_cache']).toBe(join(unitDataRoot(ROOT, 'demo'), 'npm-cache'));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('workspace 域:清单没声明就拒绝;声明后要求会话', async () => {
|
|
181
|
+
const fake = createFakeShell([{}, {}]);
|
|
182
|
+
const seam = createSeam({
|
|
183
|
+
shell: fake.shell,
|
|
184
|
+
runtimeRoot: ROOT,
|
|
185
|
+
grantOf: () => ({ mode: 'workspace-write', workspaceRoot: PROJECT }),
|
|
186
|
+
});
|
|
187
|
+
const undeclared = createUnitSeam({ seam, unit: 'demo', runtimeRoot: ROOT });
|
|
188
|
+
await expect(undeclared.run({ command: 'touch x', scope: 'workspace', session: {} })).rejects.toMatchObject({
|
|
189
|
+
code: 'scope-not-declared',
|
|
190
|
+
});
|
|
191
|
+
const declared = createUnitSeam({ seam, unit: 'demo', runtimeRoot: ROOT, declared: ['workspace'] });
|
|
192
|
+
await expect(declared.run({ command: 'touch x', scope: 'workspace' })).rejects.toMatchObject({
|
|
193
|
+
code: 'workspace-unknown',
|
|
194
|
+
});
|
|
195
|
+
await declared.run({ command: 'touch x', scope: 'workspace', session: {} });
|
|
196
|
+
expect(policyOf(fake.calls[0])).toEqual({ mode: 'workspace-write', workspaceRoot: PROJECT });
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('宿主域:run / spawn / open', () => {
|
|
201
|
+
it('host 域 run:按 argv 起宿主进程,拿回输出,记 host 事实', async () => {
|
|
202
|
+
const fake = createFakeProcesses([{ stdout: 'ok\n', exitCode: 0 }]);
|
|
203
|
+
const seam = createSeam({ shell: createFakeShell([]).shell, runtimeRoot: ROOT, allowHost: true, processes: fake.host });
|
|
204
|
+
const outcome = await seam.run({
|
|
205
|
+
argv: ['git', '-C', PROJECT, 'status'],
|
|
206
|
+
intent: { kind: 'host' },
|
|
207
|
+
env: { GIT_TERMINAL_PROMPT: '0' },
|
|
208
|
+
timeoutMs: 5_000,
|
|
209
|
+
});
|
|
210
|
+
expect(outcome.exitCode).toBe(0);
|
|
211
|
+
expect(outcome.stdout).toBe('ok\n');
|
|
212
|
+
expect(fake.calls[0]?.file).toBe('git');
|
|
213
|
+
expect(fake.calls[0]?.args).toEqual(['-C', PROJECT, 'status']);
|
|
214
|
+
expect(fake.calls[0]?.env['GIT_TERMINAL_PROMPT']).toBe('0');
|
|
215
|
+
expect(fake.calls[0]?.detached).toBe(false);
|
|
216
|
+
expect(seam.facts().at(-1)).toMatchObject({ verb: 'run', intent: 'host', mode: 'host' });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('host 域 run:shell line 走 bash -c,超时到点 kill', async () => {
|
|
220
|
+
const fake = createFakeProcesses([{ hang: true }]);
|
|
221
|
+
const seam = createSeam({
|
|
222
|
+
shell: createFakeShell([]).shell,
|
|
223
|
+
runtimeRoot: ROOT,
|
|
224
|
+
allowHost: true,
|
|
225
|
+
processes: fake.host,
|
|
226
|
+
});
|
|
227
|
+
const outcome = await seam.run({ command: 'pnpm install', shellKind: 'posix', intent: { kind: 'host' }, timeoutMs: 10 });
|
|
228
|
+
expect(fake.calls[0]?.file).toBe('bash');
|
|
229
|
+
expect(fake.calls[0]?.args).toEqual(['-c', 'pnpm install']);
|
|
230
|
+
expect(outcome.timedOut).toBe(true);
|
|
231
|
+
expect(fake.children[0]?.killed).toContain('SIGKILL');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('没有宿主能力时 host 域起不来,也不许 spawn/open', async () => {
|
|
235
|
+
const fake = createFakeProcesses([{}]);
|
|
236
|
+
const seam = createSeam({ shell: createFakeShell([]).shell, runtimeRoot: ROOT, processes: fake.host });
|
|
237
|
+
await expect(seam.run({ argv: ['git', 'status'], intent: { kind: 'host' } })).rejects.toMatchObject({
|
|
238
|
+
code: 'scope-not-declared',
|
|
239
|
+
});
|
|
240
|
+
expect(() =>
|
|
241
|
+
seam.spawn({ target: { argv: ['node', 'x'] }, intent: { kind: 'host' }, lifetime: 'host', logFile: join(SCRATCH, 'x.log') }),
|
|
242
|
+
).toThrow(SeamError);
|
|
243
|
+
expect(fake.calls).toHaveLength(0);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('spawn:detached + 日志文件 + unref,handle 能拿到 pid 与退出', async () => {
|
|
247
|
+
const fake = createFakeProcesses([{ hang: true }]);
|
|
248
|
+
const seam = createSeam({ shell: createFakeShell([]).shell, runtimeRoot: ROOT, allowHost: true, processes: fake.host });
|
|
249
|
+
const logFile = join(SCRATCH, 'spawn.log');
|
|
250
|
+
const handle = seam.spawn({
|
|
251
|
+
target: { argv: ['node', 'seed.mjs'] },
|
|
252
|
+
intent: { kind: 'host' },
|
|
253
|
+
lifetime: 'host',
|
|
254
|
+
logFile,
|
|
255
|
+
label: 'seed',
|
|
256
|
+
});
|
|
257
|
+
expect(handle.pid).toBe(4242);
|
|
258
|
+
expect(handle.logFile).toBe(logFile);
|
|
259
|
+
expect(fake.calls[0]?.detached).toBe(true);
|
|
260
|
+
expect(fake.calls[0]?.stdio).toBe('log');
|
|
261
|
+
expect(fake.calls[0]?.logFile).toBe(logFile);
|
|
262
|
+
expect(fake.children[0]?.unrefed).toBe(true);
|
|
263
|
+
expect(seam.facts().at(-1)).toMatchObject({ verb: 'spawn', intent: 'host', mode: 'host' });
|
|
264
|
+
const waiting = handle.exit();
|
|
265
|
+
fake.children[0]?.finish(0);
|
|
266
|
+
await expect(waiting).resolves.toEqual({ code: 0, signal: null });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('open:双向流读写与退出', async () => {
|
|
270
|
+
const fake = createFakeProcesses([{ hang: true }]);
|
|
271
|
+
const seam = createSeam({ shell: createFakeShell([]).shell, runtimeRoot: ROOT, allowHost: true, processes: fake.host });
|
|
272
|
+
const channel = seam.open({
|
|
273
|
+
target: { argv: ['node', 'mcp-server.mjs'] },
|
|
274
|
+
intent: { kind: 'host' },
|
|
275
|
+
env: { MCP_TOKEN: 't' },
|
|
276
|
+
});
|
|
277
|
+
const lines: string[] = [];
|
|
278
|
+
channel.onData((chunk) => lines.push(chunk));
|
|
279
|
+
channel.write('hello\n');
|
|
280
|
+
fake.children[0]?.emitStdout('world\n');
|
|
281
|
+
expect(lines).toEqual(['world\n']);
|
|
282
|
+
expect(fake.children[0]?.writes).toEqual(['hello\n']);
|
|
283
|
+
expect(fake.calls[0]?.env['MCP_TOKEN']).toBe('t');
|
|
284
|
+
const exited = new Promise<unknown>((resolve) => channel.onExit((event) => resolve(event)));
|
|
285
|
+
fake.children[0]?.finish(3);
|
|
286
|
+
await expect(exited).resolves.toEqual({ code: 3, signal: null });
|
|
287
|
+
expect(seam.facts().at(-1)).toMatchObject({ verb: 'open', intent: 'host' });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('沙箱域也能用 argv:内部拼成命令行交给 dsh shell', async () => {
|
|
291
|
+
const fake = createFakeShell([{}]);
|
|
292
|
+
const seam = createSeam({ shell: fake.shell, runtimeRoot: ROOT });
|
|
293
|
+
await seam.run({ argv: ['npm', 'view', 'left-pad', 'version'], intent: { kind: 'product' } });
|
|
294
|
+
expect(fake.calls[0]?.command).toBe("'npm' 'view' 'left-pad' 'version'");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -24,7 +24,31 @@ function replyingShell(exitCode: number, timedOut = false): DshShell {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function sandboxedShell(): DshShell {
|
|
28
|
+
return {
|
|
29
|
+
resolve: (request) => request,
|
|
30
|
+
execute: async () => ({
|
|
31
|
+
result: async () => ({
|
|
32
|
+
exitCode: 3_221_225_794,
|
|
33
|
+
timedOut: false,
|
|
34
|
+
aborted: false,
|
|
35
|
+
stdout: { text: '', truncated: false },
|
|
36
|
+
stderr: { text: 'windows-acl-run: token setup failed\n', truncated: false },
|
|
37
|
+
sandbox: { mode: 'workspace-write', denied: false, runnerFailed: true },
|
|
38
|
+
}),
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
describe('runShell', () => {
|
|
44
|
+
it('★ 沙箱事实原样带回来:runnerFailed 不当成"命令自己退出"', async () => {
|
|
45
|
+
const result = await runShell(sandboxedShell(), 'echo hi');
|
|
46
|
+
|
|
47
|
+
expect(result.exitCode).toBe(3_221_225_794);
|
|
48
|
+
expect(result.sandbox?.runnerFailed).toBe(true);
|
|
49
|
+
expect(result.stderr).toContain('windows-acl-run');
|
|
50
|
+
});
|
|
51
|
+
|
|
28
52
|
it('★ 宿主一直不交结果 → 到点自己收尾,不把调用方挂住', async () => {
|
|
29
53
|
vi.useFakeTimers();
|
|
30
54
|
try {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { clockOf, epochOf, now, nowIso } from '../time.js';
|
|
3
|
+
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.useRealTimers();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
describe('宿主时间入口', () => {
|
|
9
|
+
it('now 读的是 Date.now,nowIso 是合法 ISO', () => {
|
|
10
|
+
vi.useFakeTimers();
|
|
11
|
+
vi.setSystemTime(new Date('2026-01-02T03:04:05.000Z'));
|
|
12
|
+
expect(now()).toBe(Date.parse('2026-01-02T03:04:05.000Z'));
|
|
13
|
+
expect(nowIso()).toBe('2026-01-02T03:04:05.000Z');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('格式化助手仍只看传入值', () => {
|
|
17
|
+
expect(clockOf(0)).toBe('00:00:00');
|
|
18
|
+
expect(epochOf('1970-01-01T00:00:01.000Z')).toBe(1000);
|
|
19
|
+
});
|
|
20
|
+
});
|
package/src/host/adapter-set.ts
CHANGED
package/src/host/changelog.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { now as clockNow } from './time.js';
|
|
1
2
|
import dayjs from 'dayjs';
|
|
2
3
|
|
|
3
4
|
export const CHANGELOG_FILE = 'CHANGELOG.md';
|
|
@@ -37,7 +38,7 @@ export function changelogVersion(value: string): string {
|
|
|
37
38
|
.replace(/^v/, '');
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
export function changelogDate(now: Date = new Date()): string {
|
|
41
|
+
export function changelogDate(now: Date = new Date(clockNow())): string {
|
|
41
42
|
return dayjs(now).format('YYYY-MM-DD');
|
|
42
43
|
}
|
|
43
44
|
|