opc-agent 4.1.0 → 4.1.2
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/.github/ISSUE_TEMPLATE/bug_report.md +20 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -14
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -13
- package/CHANGELOG.md +48 -48
- package/CONTRIBUTING.md +36 -36
- package/README.zh-CN.md +497 -497
- package/USABILITY-ISSUES.md +73 -0
- package/dist/channels/web.js +8 -2
- package/dist/channels/wechat.js +6 -6
- package/dist/cli.js +200 -85
- package/dist/core/runtime.js +37 -15
- package/dist/deploy/index.js +56 -56
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +105 -10
- package/dist/memory/deepbrain.d.ts +1 -1
- package/dist/memory/deepbrain.js +95 -4
- package/dist/scheduler/cron-engine.js +3 -36
- package/dist/studio/server.js +30 -1
- package/dist/studio-ui/index.html +230 -10
- package/dist/ui/components.js +105 -105
- package/examples/README.md +22 -22
- package/examples/basic-agent.ts +90 -90
- package/examples/brain-integration.ts +71 -71
- package/examples/multi-channel.ts +74 -74
- package/fix-sidebar.mjs +188 -188
- package/install.ps1 +154 -154
- package/install.sh +164 -164
- package/package.json +1 -1
- package/scripts/install.ps1 +31 -31
- package/scripts/install.sh +40 -40
- package/serve-studio.js +13 -13
- package/serve-test.js +25 -25
- package/src/channels/dingtalk.ts +46 -46
- package/src/channels/email.ts +351 -351
- package/src/channels/feishu.ts +349 -349
- package/src/channels/googlechat.ts +42 -42
- package/src/channels/imessage.ts +31 -31
- package/src/channels/irc.ts +82 -82
- package/src/channels/line.ts +32 -32
- package/src/channels/matrix.ts +33 -33
- package/src/channels/mattermost.ts +57 -57
- package/src/channels/msteams.ts +32 -32
- package/src/channels/nostr.ts +32 -32
- package/src/channels/qq.ts +33 -33
- package/src/channels/signal.ts +32 -32
- package/src/channels/sms.ts +33 -33
- package/src/channels/telegram.ts +616 -616
- package/src/channels/twitch.ts +65 -65
- package/src/channels/voice-call.ts +100 -100
- package/src/channels/web.ts +8 -2
- package/src/channels/websocket.ts +399 -399
- package/src/channels/wechat.ts +329 -329
- package/src/channels/whatsapp.ts +32 -32
- package/src/cli/chat.ts +99 -99
- package/src/cli/setup.ts +314 -314
- package/src/cli.ts +195 -92
- package/src/core/agent.ts +476 -476
- package/src/core/api-server.ts +277 -277
- package/src/core/audio.ts +98 -98
- package/src/core/collaboration.ts +275 -275
- package/src/core/context-discovery.ts +85 -85
- package/src/core/context-refs.ts +140 -140
- package/src/core/gateway.ts +106 -106
- package/src/core/heartbeat.ts +51 -51
- package/src/core/hooks.ts +105 -105
- package/src/core/ide-bridge.ts +133 -133
- package/src/core/node-network.ts +86 -86
- package/src/core/profiles.ts +122 -122
- package/src/core/runtime.ts +25 -0
- package/src/core/scheduler.ts +187 -187
- package/src/core/session-manager.ts +137 -137
- package/src/core/subagent.ts +98 -98
- package/src/core/vision.ts +180 -180
- package/src/core/workflow-graph.ts +365 -365
- package/src/daemon.ts +96 -96
- package/src/deploy/index.ts +255 -255
- package/src/doctor.ts +98 -11
- package/src/eval/index.ts +211 -211
- package/src/eval/suites/basic.json +16 -16
- package/src/eval/suites/memory.json +12 -12
- package/src/eval/suites/safety.json +14 -14
- package/src/hub/brain-seed.ts +54 -54
- package/src/hub/client.ts +60 -60
- package/src/mcp/servers/calculator-mcp.ts +65 -65
- package/src/mcp/servers/crypto-mcp.ts +73 -73
- package/src/mcp/servers/database-mcp.ts +72 -72
- package/src/mcp/servers/datetime-mcp.ts +69 -69
- package/src/mcp/servers/filesystem.ts +66 -66
- package/src/mcp/servers/github-mcp.ts +58 -58
- package/src/mcp/servers/index.ts +63 -63
- package/src/mcp/servers/json-mcp.ts +102 -102
- package/src/mcp/servers/memory-mcp.ts +56 -56
- package/src/mcp/servers/regex-mcp.ts +53 -53
- package/src/mcp/servers/web-mcp.ts +49 -49
- package/src/memory/context-compressor.ts +189 -189
- package/src/memory/deepbrain.ts +99 -5
- package/src/memory/seed-loader.ts +212 -212
- package/src/memory/user-profiler.ts +215 -215
- package/src/plugins/content-filter.ts +23 -23
- package/src/plugins/logger.ts +18 -18
- package/src/plugins/rate-limiter.ts +38 -38
- package/src/protocols/a2a/client.ts +132 -132
- package/src/protocols/a2a/index.ts +8 -8
- package/src/protocols/a2a/server.ts +333 -333
- package/src/protocols/a2a/types.ts +88 -88
- package/src/protocols/a2a/utils.ts +50 -50
- package/src/protocols/agui/client.ts +83 -83
- package/src/protocols/agui/index.ts +4 -4
- package/src/protocols/agui/server.ts +218 -218
- package/src/protocols/agui/types.ts +153 -153
- package/src/protocols/index.ts +2 -2
- package/src/protocols/mcp/agent-tools.ts +134 -134
- package/src/protocols/mcp/index.ts +8 -8
- package/src/protocols/mcp/server.ts +262 -262
- package/src/protocols/mcp/types.ts +69 -69
- package/src/providers/index.ts +632 -632
- package/src/publish/index.ts +376 -376
- package/src/scheduler/cron-engine.ts +191 -191
- package/src/scheduler/index.ts +2 -2
- package/src/schema/oad.ts +217 -217
- package/src/security/approval.ts +131 -131
- package/src/security/approvals.ts +143 -143
- package/src/security/elevated.ts +105 -105
- package/src/security/guardrails.ts +248 -248
- package/src/security/index.ts +9 -9
- package/src/security/keys.ts +87 -87
- package/src/security/secrets.ts +129 -129
- package/src/skills/builtin/index.ts +408 -408
- package/src/skills/marketplace.ts +113 -113
- package/src/skills/types.ts +42 -42
- package/src/studio/server.ts +31 -1
- package/src/studio/templates-data.ts +178 -178
- package/src/studio-ui/index.html +230 -10
- package/src/telemetry/index.ts +324 -324
- package/src/tools/builtin/browser.ts +299 -299
- package/src/tools/builtin/datetime.ts +41 -41
- package/src/tools/builtin/file.ts +107 -107
- package/src/tools/builtin/home-assistant.ts +116 -116
- package/src/tools/builtin/rl-tools.ts +243 -243
- package/src/tools/builtin/shell.ts +43 -43
- package/src/tools/builtin/vision.ts +64 -64
- package/src/tools/builtin/web-search.ts +126 -126
- package/src/tools/builtin/web.ts +35 -35
- package/src/tools/document-processor.ts +213 -213
- package/src/tools/image-generator.ts +150 -150
- package/src/tools/integrations/calendar.ts +73 -73
- package/src/tools/integrations/code-exec.ts +39 -39
- package/src/tools/integrations/csv-analyzer.ts +92 -92
- package/src/tools/integrations/database.ts +44 -44
- package/src/tools/integrations/email-send.ts +76 -76
- package/src/tools/integrations/git-tool.ts +42 -42
- package/src/tools/integrations/github-tool.ts +76 -76
- package/src/tools/integrations/image-gen.ts +56 -56
- package/src/tools/integrations/index.ts +92 -92
- package/src/tools/integrations/jira.ts +83 -83
- package/src/tools/integrations/notion.ts +71 -71
- package/src/tools/integrations/npm-tool.ts +48 -48
- package/src/tools/integrations/pdf-reader.ts +58 -58
- package/src/tools/integrations/slack.ts +65 -65
- package/src/tools/integrations/summarizer.ts +49 -49
- package/src/tools/integrations/translator.ts +48 -48
- package/src/tools/integrations/trello.ts +60 -60
- package/src/tools/integrations/vector-search.ts +42 -42
- package/src/tools/integrations/web-scraper.ts +47 -47
- package/src/tools/integrations/web-search.ts +58 -58
- package/src/tools/integrations/webhook.ts +38 -38
- package/src/tools/mcp-client.ts +131 -131
- package/src/tools/web-scraper.ts +179 -179
- package/src/tools/web-search.ts +180 -180
- package/src/ui/components.ts +127 -127
- package/srv-out.txt +1 -1
- package/templates/ecommerce-assistant/README.md +45 -45
- package/templates/ecommerce-assistant/oad.yaml +47 -47
- package/templates/tech-support/README.md +43 -43
- package/templates/tech-support/oad.yaml +45 -45
- package/test-agent/Dockerfile +9 -9
- package/test-agent/README.md +50 -50
- package/test-agent/agent.yaml +23 -23
- package/test-agent/docker-compose.yml +11 -11
- package/test-agent/oad.yaml +31 -31
- package/test-agent/package-lock.json +1492 -1492
- package/test-agent/package.json +17 -17
- package/test-agent/src/index.ts +24 -24
- package/test-agent/src/skills/echo.ts +15 -15
- package/test-agent/tsconfig.json +24 -24
- package/test-full.js +43 -43
- package/test-sidebar.js +22 -22
- package/test-studio3.js +75 -75
- package/test-studio4.js +41 -41
- package/tests/a2a-protocol.test.ts +285 -285
- package/tests/agui-protocol.test.ts +246 -246
- package/tests/api-server.test.ts +148 -148
- package/tests/approvals.test.ts +89 -89
- package/tests/audio.test.ts +40 -40
- package/tests/brain-seed-extended.test.ts +490 -490
- package/tests/brain-seed.test.ts +239 -239
- package/tests/browser.test.ts +179 -179
- package/tests/channels/discord.test.ts +79 -79
- package/tests/channels/email.test.ts +148 -148
- package/tests/channels/feishu.test.ts +123 -123
- package/tests/channels/telegram.test.ts +129 -129
- package/tests/channels/websocket.test.ts +53 -53
- package/tests/channels/wechat.test.ts +170 -170
- package/tests/channels-extra.test.ts +45 -45
- package/tests/chat-cli.test.ts +160 -160
- package/tests/cli.test.ts +46 -46
- package/tests/context-compressor.test.ts +172 -172
- package/tests/context-refs.test.ts +121 -121
- package/tests/cron-engine.test.ts +101 -101
- package/tests/daemon.test.ts +135 -135
- package/tests/deepbrain-wire.test.ts +234 -234
- package/tests/deploy-and-dag.test.ts +196 -196
- package/tests/doctor.test.ts +38 -38
- package/tests/document-processor.test.ts +69 -69
- package/tests/e2e-nocode.test.ts +442 -442
- package/tests/elevated.test.ts +69 -69
- package/tests/eval.test.ts +173 -173
- package/tests/gateway.test.ts +63 -63
- package/tests/guardrails.test.ts +177 -177
- package/tests/home-assistant.test.ts +40 -40
- package/tests/hooks.test.ts +79 -79
- package/tests/ide-bridge.test.ts +38 -38
- package/tests/image-generator.test.ts +84 -84
- package/tests/init-role.test.ts +124 -124
- package/tests/integrations.test.ts +249 -249
- package/tests/mcp-client.test.ts +92 -92
- package/tests/mcp-server.test.ts +178 -178
- package/tests/mcp-servers.test.ts +260 -260
- package/tests/node-network.test.ts +74 -74
- package/tests/plugin-a2a-enhanced.test.ts +230 -230
- package/tests/profiles.test.ts +61 -61
- package/tests/publish.test.ts +231 -231
- package/tests/rl-tools.test.ts +93 -93
- package/tests/sandbox-manager.test.ts +46 -46
- package/tests/scheduler.test.ts +200 -200
- package/tests/secrets.test.ts +107 -107
- package/tests/security-enhanced.test.ts +233 -233
- package/tests/settings-api.test.ts +148 -148
- package/tests/setup.test.ts +73 -73
- package/tests/subagent.test.ts +193 -193
- package/tests/telegram-discord.test.ts +60 -60
- package/tests/telemetry.test.ts +186 -186
- package/tests/user-profiler.test.ts +169 -169
- package/tests/v090-features.test.ts +254 -254
- package/tests/vision.test.ts +61 -61
- package/tests/voice-call.test.ts +47 -47
- package/tests/voice-enhanced.test.ts +169 -169
- package/tests/voice-interaction.test.ts +38 -38
- package/tests/web-search.test.ts +155 -155
- package/tests/workflow-graph.test.ts +279 -279
- package/tutorial/customer-service-agent/README.md +612 -612
- package/tutorial/customer-service-agent/SOUL.md +26 -26
- package/tutorial/customer-service-agent/agent.yaml +63 -63
- package/tutorial/customer-service-agent/package.json +19 -19
- package/tutorial/customer-service-agent/src/index.ts +69 -69
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -27
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -22
- package/tutorial/customer-service-agent/tsconfig.json +14 -14
package/tests/scheduler.test.ts
CHANGED
|
@@ -1,200 +1,200 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { parseCron, cronMatches, Scheduler } from '../src/core/scheduler';
|
|
3
|
-
import type { CronJob } from '../src/core/scheduler';
|
|
4
|
-
|
|
5
|
-
describe('parseCron', () => {
|
|
6
|
-
it('parses "* * * * *" as all-any fields', () => {
|
|
7
|
-
const p = parseCron('* * * * *');
|
|
8
|
-
expect(p.minute).toEqual({ type: 'any' });
|
|
9
|
-
expect(p.hour).toEqual({ type: 'any' });
|
|
10
|
-
expect(p.dayOfMonth).toEqual({ type: 'any' });
|
|
11
|
-
expect(p.month).toEqual({ type: 'any' });
|
|
12
|
-
expect(p.dayOfWeek).toEqual({ type: 'any' });
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('parses "0 9 * * *" as minute=0, hour=9', () => {
|
|
16
|
-
const p = parseCron('0 9 * * *');
|
|
17
|
-
expect(p.minute).toEqual({ type: 'list', values: [0] });
|
|
18
|
-
expect(p.hour).toEqual({ type: 'list', values: [9] });
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('parses "*/5 * * * *" as every-5 minutes', () => {
|
|
22
|
-
const p = parseCron('*/5 * * * *');
|
|
23
|
-
expect(p.minute).toEqual({ type: 'every', step: 5 });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('parses "0 9 * * 1" for Monday 9:00', () => {
|
|
27
|
-
const p = parseCron('0 9 * * 1');
|
|
28
|
-
expect(p.dayOfWeek).toEqual({ type: 'list', values: [1] });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('parses "0 9-17 * * *" as hour range 9-17', () => {
|
|
32
|
-
const p = parseCron('0 9-17 * * *');
|
|
33
|
-
expect(p.hour).toEqual({ type: 'list', values: [9, 10, 11, 12, 13, 14, 15, 16, 17] });
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('parses "0 9,12,18 * * *" as specific hours', () => {
|
|
37
|
-
const p = parseCron('0 9,12,18 * * *');
|
|
38
|
-
expect(p.hour).toEqual({ type: 'list', values: [9, 12, 18] });
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('throws on invalid cron expression with wrong field count', () => {
|
|
42
|
-
expect(() => parseCron('* * *')).toThrow('Invalid cron expression');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('throws on invalid step value', () => {
|
|
46
|
-
expect(() => parseCron('*/abc * * * *')).toThrow();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('throws on invalid range', () => {
|
|
50
|
-
expect(() => parseCron('a-b * * * *')).toThrow();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('parses "30 */2 * * *" correctly', () => {
|
|
54
|
-
const p = parseCron('30 */2 * * *');
|
|
55
|
-
expect(p.minute).toEqual({ type: 'list', values: [30] });
|
|
56
|
-
expect(p.hour).toEqual({ type: 'every', step: 2 });
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe('cronMatches', () => {
|
|
61
|
-
it('"* * * * *" matches any date', () => {
|
|
62
|
-
const p = parseCron('* * * * *');
|
|
63
|
-
expect(cronMatches(p, new Date('2026-04-18T10:30:00'))).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('"0 9 * * *" matches 9:00 but not 10:00', () => {
|
|
67
|
-
const p = parseCron('0 9 * * *');
|
|
68
|
-
expect(cronMatches(p, new Date('2026-04-18T09:00:00'))).toBe(true);
|
|
69
|
-
expect(cronMatches(p, new Date('2026-04-18T10:00:00'))).toBe(false);
|
|
70
|
-
expect(cronMatches(p, new Date('2026-04-18T09:05:00'))).toBe(false);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('"*/5 * * * *" matches minutes divisible by 5', () => {
|
|
74
|
-
const p = parseCron('*/5 * * * *');
|
|
75
|
-
expect(cronMatches(p, new Date('2026-04-18T10:00:00'))).toBe(true);
|
|
76
|
-
expect(cronMatches(p, new Date('2026-04-18T10:05:00'))).toBe(true);
|
|
77
|
-
expect(cronMatches(p, new Date('2026-04-18T10:03:00'))).toBe(false);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('"0 9 * * 1" matches Monday 9:00 only', () => {
|
|
81
|
-
const p = parseCron('0 9 * * 1');
|
|
82
|
-
// 2026-04-20 is Monday
|
|
83
|
-
expect(cronMatches(p, new Date('2026-04-20T09:00:00'))).toBe(true);
|
|
84
|
-
// 2026-04-18 is Saturday
|
|
85
|
-
expect(cronMatches(p, new Date('2026-04-18T09:00:00'))).toBe(false);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('"0 9-17 * * *" matches hours 9 through 17', () => {
|
|
89
|
-
const p = parseCron('0 9-17 * * *');
|
|
90
|
-
expect(cronMatches(p, new Date('2026-04-18T09:00:00'))).toBe(true);
|
|
91
|
-
expect(cronMatches(p, new Date('2026-04-18T17:00:00'))).toBe(true);
|
|
92
|
-
expect(cronMatches(p, new Date('2026-04-18T08:00:00'))).toBe(false);
|
|
93
|
-
expect(cronMatches(p, new Date('2026-04-18T18:00:00'))).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('Scheduler', () => {
|
|
98
|
-
let scheduler: Scheduler;
|
|
99
|
-
let handler: ReturnType<typeof vi.fn>;
|
|
100
|
-
|
|
101
|
-
beforeEach(() => {
|
|
102
|
-
vi.useFakeTimers();
|
|
103
|
-
handler = vi.fn();
|
|
104
|
-
scheduler = new Scheduler(handler);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
afterEach(() => {
|
|
108
|
-
scheduler.stop();
|
|
109
|
-
vi.useRealTimers();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const makeJob = (id: string, schedule = '* * * * *', enabled = true): CronJob => ({
|
|
113
|
-
id, name: `job-${id}`, schedule, task: `task-${id}`, enabled,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('addJob adds a job retrievable by getJobs', () => {
|
|
117
|
-
scheduler.addJob(makeJob('j1'));
|
|
118
|
-
expect(scheduler.getJobs()).toHaveLength(1);
|
|
119
|
-
expect(scheduler.getJobs()[0].id).toBe('j1');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('removeJob removes a job', () => {
|
|
123
|
-
scheduler.addJob(makeJob('j1'));
|
|
124
|
-
scheduler.removeJob('j1');
|
|
125
|
-
expect(scheduler.getJobs()).toHaveLength(0);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('enableJob / disableJob toggles enabled', () => {
|
|
129
|
-
scheduler.addJob(makeJob('j1', '* * * * *', false));
|
|
130
|
-
expect(scheduler.getJob('j1')!.enabled).toBe(false);
|
|
131
|
-
scheduler.enableJob('j1');
|
|
132
|
-
expect(scheduler.getJob('j1')!.enabled).toBe(true);
|
|
133
|
-
scheduler.disableJob('j1');
|
|
134
|
-
expect(scheduler.getJob('j1')!.enabled).toBe(false);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('getJobs returns all jobs', () => {
|
|
138
|
-
scheduler.addJob(makeJob('j1'));
|
|
139
|
-
scheduler.addJob(makeJob('j2'));
|
|
140
|
-
scheduler.addJob(makeJob('j3'));
|
|
141
|
-
expect(scheduler.getJobs()).toHaveLength(3);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('start begins ticking (handler called on matching cron)', () => {
|
|
145
|
-
vi.setSystemTime(new Date('2026-04-18T10:00:00'));
|
|
146
|
-
scheduler.addJob(makeJob('j1', '* * * * *'));
|
|
147
|
-
scheduler.start();
|
|
148
|
-
// tick() called immediately on start
|
|
149
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('stop stops ticking', () => {
|
|
153
|
-
vi.setSystemTime(new Date('2026-04-18T10:00:00'));
|
|
154
|
-
scheduler.addJob(makeJob('j1', '* * * * *'));
|
|
155
|
-
scheduler.start();
|
|
156
|
-
handler.mockClear();
|
|
157
|
-
scheduler.stop();
|
|
158
|
-
vi.advanceTimersByTime(120_000);
|
|
159
|
-
expect(handler).not.toHaveBeenCalled();
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('disabled job is not fired', () => {
|
|
163
|
-
vi.setSystemTime(new Date('2026-04-18T10:00:00'));
|
|
164
|
-
scheduler.addJob(makeJob('j1', '* * * * *', false));
|
|
165
|
-
scheduler.start();
|
|
166
|
-
expect(handler).not.toHaveBeenCalled();
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('double-fire prevention within same minute', () => {
|
|
170
|
-
vi.setSystemTime(new Date('2026-04-18T10:00:00'));
|
|
171
|
-
scheduler.addJob(makeJob('j1', '* * * * *'));
|
|
172
|
-
scheduler.start();
|
|
173
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
174
|
-
// Advance 30 seconds (still same minute), force another tick
|
|
175
|
-
vi.advanceTimersByTime(30_000);
|
|
176
|
-
// No additional call because same minute
|
|
177
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('job fires again in next minute', () => {
|
|
181
|
-
vi.setSystemTime(new Date('2026-04-18T10:00:00'));
|
|
182
|
-
scheduler.addJob(makeJob('j1', '* * * * *'));
|
|
183
|
-
scheduler.start();
|
|
184
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
185
|
-
vi.advanceTimersByTime(60_000);
|
|
186
|
-
expect(handler).toHaveBeenCalledTimes(2);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('runJob fires handler immediately', async () => {
|
|
190
|
-
scheduler.addJob(makeJob('j1', '0 0 1 1 *')); // rare schedule
|
|
191
|
-
const result = await scheduler.runJob('j1');
|
|
192
|
-
expect(result).toBe(true);
|
|
193
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('runJob returns false for unknown job', async () => {
|
|
197
|
-
const result = await scheduler.runJob('nonexistent');
|
|
198
|
-
expect(result).toBe(false);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { parseCron, cronMatches, Scheduler } from '../src/core/scheduler';
|
|
3
|
+
import type { CronJob } from '../src/core/scheduler';
|
|
4
|
+
|
|
5
|
+
describe('parseCron', () => {
|
|
6
|
+
it('parses "* * * * *" as all-any fields', () => {
|
|
7
|
+
const p = parseCron('* * * * *');
|
|
8
|
+
expect(p.minute).toEqual({ type: 'any' });
|
|
9
|
+
expect(p.hour).toEqual({ type: 'any' });
|
|
10
|
+
expect(p.dayOfMonth).toEqual({ type: 'any' });
|
|
11
|
+
expect(p.month).toEqual({ type: 'any' });
|
|
12
|
+
expect(p.dayOfWeek).toEqual({ type: 'any' });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('parses "0 9 * * *" as minute=0, hour=9', () => {
|
|
16
|
+
const p = parseCron('0 9 * * *');
|
|
17
|
+
expect(p.minute).toEqual({ type: 'list', values: [0] });
|
|
18
|
+
expect(p.hour).toEqual({ type: 'list', values: [9] });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('parses "*/5 * * * *" as every-5 minutes', () => {
|
|
22
|
+
const p = parseCron('*/5 * * * *');
|
|
23
|
+
expect(p.minute).toEqual({ type: 'every', step: 5 });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('parses "0 9 * * 1" for Monday 9:00', () => {
|
|
27
|
+
const p = parseCron('0 9 * * 1');
|
|
28
|
+
expect(p.dayOfWeek).toEqual({ type: 'list', values: [1] });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('parses "0 9-17 * * *" as hour range 9-17', () => {
|
|
32
|
+
const p = parseCron('0 9-17 * * *');
|
|
33
|
+
expect(p.hour).toEqual({ type: 'list', values: [9, 10, 11, 12, 13, 14, 15, 16, 17] });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('parses "0 9,12,18 * * *" as specific hours', () => {
|
|
37
|
+
const p = parseCron('0 9,12,18 * * *');
|
|
38
|
+
expect(p.hour).toEqual({ type: 'list', values: [9, 12, 18] });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('throws on invalid cron expression with wrong field count', () => {
|
|
42
|
+
expect(() => parseCron('* * *')).toThrow('Invalid cron expression');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('throws on invalid step value', () => {
|
|
46
|
+
expect(() => parseCron('*/abc * * * *')).toThrow();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('throws on invalid range', () => {
|
|
50
|
+
expect(() => parseCron('a-b * * * *')).toThrow();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('parses "30 */2 * * *" correctly', () => {
|
|
54
|
+
const p = parseCron('30 */2 * * *');
|
|
55
|
+
expect(p.minute).toEqual({ type: 'list', values: [30] });
|
|
56
|
+
expect(p.hour).toEqual({ type: 'every', step: 2 });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('cronMatches', () => {
|
|
61
|
+
it('"* * * * *" matches any date', () => {
|
|
62
|
+
const p = parseCron('* * * * *');
|
|
63
|
+
expect(cronMatches(p, new Date('2026-04-18T10:30:00'))).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('"0 9 * * *" matches 9:00 but not 10:00', () => {
|
|
67
|
+
const p = parseCron('0 9 * * *');
|
|
68
|
+
expect(cronMatches(p, new Date('2026-04-18T09:00:00'))).toBe(true);
|
|
69
|
+
expect(cronMatches(p, new Date('2026-04-18T10:00:00'))).toBe(false);
|
|
70
|
+
expect(cronMatches(p, new Date('2026-04-18T09:05:00'))).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('"*/5 * * * *" matches minutes divisible by 5', () => {
|
|
74
|
+
const p = parseCron('*/5 * * * *');
|
|
75
|
+
expect(cronMatches(p, new Date('2026-04-18T10:00:00'))).toBe(true);
|
|
76
|
+
expect(cronMatches(p, new Date('2026-04-18T10:05:00'))).toBe(true);
|
|
77
|
+
expect(cronMatches(p, new Date('2026-04-18T10:03:00'))).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('"0 9 * * 1" matches Monday 9:00 only', () => {
|
|
81
|
+
const p = parseCron('0 9 * * 1');
|
|
82
|
+
// 2026-04-20 is Monday
|
|
83
|
+
expect(cronMatches(p, new Date('2026-04-20T09:00:00'))).toBe(true);
|
|
84
|
+
// 2026-04-18 is Saturday
|
|
85
|
+
expect(cronMatches(p, new Date('2026-04-18T09:00:00'))).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('"0 9-17 * * *" matches hours 9 through 17', () => {
|
|
89
|
+
const p = parseCron('0 9-17 * * *');
|
|
90
|
+
expect(cronMatches(p, new Date('2026-04-18T09:00:00'))).toBe(true);
|
|
91
|
+
expect(cronMatches(p, new Date('2026-04-18T17:00:00'))).toBe(true);
|
|
92
|
+
expect(cronMatches(p, new Date('2026-04-18T08:00:00'))).toBe(false);
|
|
93
|
+
expect(cronMatches(p, new Date('2026-04-18T18:00:00'))).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('Scheduler', () => {
|
|
98
|
+
let scheduler: Scheduler;
|
|
99
|
+
let handler: ReturnType<typeof vi.fn>;
|
|
100
|
+
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
vi.useFakeTimers();
|
|
103
|
+
handler = vi.fn();
|
|
104
|
+
scheduler = new Scheduler(handler);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
scheduler.stop();
|
|
109
|
+
vi.useRealTimers();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const makeJob = (id: string, schedule = '* * * * *', enabled = true): CronJob => ({
|
|
113
|
+
id, name: `job-${id}`, schedule, task: `task-${id}`, enabled,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('addJob adds a job retrievable by getJobs', () => {
|
|
117
|
+
scheduler.addJob(makeJob('j1'));
|
|
118
|
+
expect(scheduler.getJobs()).toHaveLength(1);
|
|
119
|
+
expect(scheduler.getJobs()[0].id).toBe('j1');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('removeJob removes a job', () => {
|
|
123
|
+
scheduler.addJob(makeJob('j1'));
|
|
124
|
+
scheduler.removeJob('j1');
|
|
125
|
+
expect(scheduler.getJobs()).toHaveLength(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('enableJob / disableJob toggles enabled', () => {
|
|
129
|
+
scheduler.addJob(makeJob('j1', '* * * * *', false));
|
|
130
|
+
expect(scheduler.getJob('j1')!.enabled).toBe(false);
|
|
131
|
+
scheduler.enableJob('j1');
|
|
132
|
+
expect(scheduler.getJob('j1')!.enabled).toBe(true);
|
|
133
|
+
scheduler.disableJob('j1');
|
|
134
|
+
expect(scheduler.getJob('j1')!.enabled).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('getJobs returns all jobs', () => {
|
|
138
|
+
scheduler.addJob(makeJob('j1'));
|
|
139
|
+
scheduler.addJob(makeJob('j2'));
|
|
140
|
+
scheduler.addJob(makeJob('j3'));
|
|
141
|
+
expect(scheduler.getJobs()).toHaveLength(3);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('start begins ticking (handler called on matching cron)', () => {
|
|
145
|
+
vi.setSystemTime(new Date('2026-04-18T10:00:00'));
|
|
146
|
+
scheduler.addJob(makeJob('j1', '* * * * *'));
|
|
147
|
+
scheduler.start();
|
|
148
|
+
// tick() called immediately on start
|
|
149
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('stop stops ticking', () => {
|
|
153
|
+
vi.setSystemTime(new Date('2026-04-18T10:00:00'));
|
|
154
|
+
scheduler.addJob(makeJob('j1', '* * * * *'));
|
|
155
|
+
scheduler.start();
|
|
156
|
+
handler.mockClear();
|
|
157
|
+
scheduler.stop();
|
|
158
|
+
vi.advanceTimersByTime(120_000);
|
|
159
|
+
expect(handler).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('disabled job is not fired', () => {
|
|
163
|
+
vi.setSystemTime(new Date('2026-04-18T10:00:00'));
|
|
164
|
+
scheduler.addJob(makeJob('j1', '* * * * *', false));
|
|
165
|
+
scheduler.start();
|
|
166
|
+
expect(handler).not.toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('double-fire prevention within same minute', () => {
|
|
170
|
+
vi.setSystemTime(new Date('2026-04-18T10:00:00'));
|
|
171
|
+
scheduler.addJob(makeJob('j1', '* * * * *'));
|
|
172
|
+
scheduler.start();
|
|
173
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
174
|
+
// Advance 30 seconds (still same minute), force another tick
|
|
175
|
+
vi.advanceTimersByTime(30_000);
|
|
176
|
+
// No additional call because same minute
|
|
177
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('job fires again in next minute', () => {
|
|
181
|
+
vi.setSystemTime(new Date('2026-04-18T10:00:00'));
|
|
182
|
+
scheduler.addJob(makeJob('j1', '* * * * *'));
|
|
183
|
+
scheduler.start();
|
|
184
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
185
|
+
vi.advanceTimersByTime(60_000);
|
|
186
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('runJob fires handler immediately', async () => {
|
|
190
|
+
scheduler.addJob(makeJob('j1', '0 0 1 1 *')); // rare schedule
|
|
191
|
+
const result = await scheduler.runJob('j1');
|
|
192
|
+
expect(result).toBe(true);
|
|
193
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('runJob returns false for unknown job', async () => {
|
|
197
|
+
const result = await scheduler.runJob('nonexistent');
|
|
198
|
+
expect(result).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
});
|
package/tests/secrets.test.ts
CHANGED
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
-
import { SecretsManager } from '../src/security/secrets';
|
|
3
|
-
import { existsSync, unlinkSync, mkdirSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { tmpdir } from 'os';
|
|
6
|
-
|
|
7
|
-
const testDir = join(tmpdir(), 'opc-secrets-test-' + Date.now());
|
|
8
|
-
let counter = 0;
|
|
9
|
-
function testPath() { return join(testDir, `secrets-${++counter}.enc`); }
|
|
10
|
-
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
// cleanup
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
describe('SecretsManager', () => {
|
|
16
|
-
it('should create new store and set/get secrets', () => {
|
|
17
|
-
const fp = testPath();
|
|
18
|
-
const mgr = new SecretsManager({ password: 'test123', filePath: fp });
|
|
19
|
-
mgr.set('API_KEY', 'sk-abc123');
|
|
20
|
-
expect(mgr.get('API_KEY')).toBe('sk-abc123');
|
|
21
|
-
expect(existsSync(fp)).toBe(true);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('should persist and reload secrets', () => {
|
|
25
|
-
const fp = testPath();
|
|
26
|
-
const mgr1 = new SecretsManager({ password: 'pw', filePath: fp });
|
|
27
|
-
mgr1.set('TOKEN', 'xyz');
|
|
28
|
-
const mgr2 = new SecretsManager({ password: 'pw', filePath: fp });
|
|
29
|
-
expect(mgr2.get('TOKEN')).toBe('xyz');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should fail with wrong password', () => {
|
|
33
|
-
const fp = testPath();
|
|
34
|
-
const mgr = new SecretsManager({ password: 'right', filePath: fp });
|
|
35
|
-
mgr.set('KEY', 'val');
|
|
36
|
-
expect(() => new SecretsManager({ password: 'wrong', filePath: fp })).toThrow();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should delete secrets', () => {
|
|
40
|
-
const fp = testPath();
|
|
41
|
-
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
42
|
-
mgr.set('A', '1');
|
|
43
|
-
expect(mgr.delete('A')).toBe(true);
|
|
44
|
-
expect(mgr.get('A')).toBeUndefined();
|
|
45
|
-
expect(mgr.delete('nonexistent')).toBe(false);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('should list secret keys', () => {
|
|
49
|
-
const fp = testPath();
|
|
50
|
-
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
51
|
-
mgr.set('A', '1');
|
|
52
|
-
mgr.set('B', '2');
|
|
53
|
-
expect(mgr.list().sort()).toEqual(['A', 'B']);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should check has', () => {
|
|
57
|
-
const fp = testPath();
|
|
58
|
-
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
59
|
-
mgr.set('X', 'y');
|
|
60
|
-
expect(mgr.has('X')).toBe(true);
|
|
61
|
-
expect(mgr.has('Z')).toBe(false);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should inject secrets into env object', () => {
|
|
65
|
-
const fp = testPath();
|
|
66
|
-
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
67
|
-
mgr.set('DB_HOST', 'localhost');
|
|
68
|
-
mgr.set('DB_PASS', 'secret');
|
|
69
|
-
const env: Record<string, string | undefined> = { PATH: '/usr/bin' };
|
|
70
|
-
mgr.inject(env, ['DB_HOST']);
|
|
71
|
-
expect(env.DB_HOST).toBe('localhost');
|
|
72
|
-
expect(env.DB_PASS).toBeUndefined();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should inject all secrets when no keys specified', () => {
|
|
76
|
-
const fp = testPath();
|
|
77
|
-
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
78
|
-
mgr.set('A', '1');
|
|
79
|
-
mgr.set('B', '2');
|
|
80
|
-
const env: Record<string, string | undefined> = {};
|
|
81
|
-
mgr.inject(env);
|
|
82
|
-
expect(env.A).toBe('1');
|
|
83
|
-
expect(env.B).toBe('2');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should rotate password', () => {
|
|
87
|
-
const fp = testPath();
|
|
88
|
-
const mgr = new SecretsManager({ password: 'old', filePath: fp });
|
|
89
|
-
mgr.set('KEY', 'value');
|
|
90
|
-
mgr.rotate('new');
|
|
91
|
-
// Old password should fail
|
|
92
|
-
expect(() => new SecretsManager({ password: 'old', filePath: fp })).toThrow();
|
|
93
|
-
// New password should work
|
|
94
|
-
const mgr2 = new SecretsManager({ password: 'new', filePath: fp });
|
|
95
|
-
expect(mgr2.get('KEY')).toBe('value');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should export and import', () => {
|
|
99
|
-
const fp1 = testPath();
|
|
100
|
-
const mgr = new SecretsManager({ password: 'pw', filePath: fp1 });
|
|
101
|
-
mgr.set('SECRET', 'data');
|
|
102
|
-
const exported = mgr.exportEncrypted();
|
|
103
|
-
const fp2 = testPath();
|
|
104
|
-
const mgr2 = SecretsManager.importEncrypted(exported, 'pw', fp2);
|
|
105
|
-
expect(mgr2.get('SECRET')).toBe('data');
|
|
106
|
-
});
|
|
107
|
-
});
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { SecretsManager } from '../src/security/secrets';
|
|
3
|
+
import { existsSync, unlinkSync, mkdirSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
|
|
7
|
+
const testDir = join(tmpdir(), 'opc-secrets-test-' + Date.now());
|
|
8
|
+
let counter = 0;
|
|
9
|
+
function testPath() { return join(testDir, `secrets-${++counter}.enc`); }
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
// cleanup
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('SecretsManager', () => {
|
|
16
|
+
it('should create new store and set/get secrets', () => {
|
|
17
|
+
const fp = testPath();
|
|
18
|
+
const mgr = new SecretsManager({ password: 'test123', filePath: fp });
|
|
19
|
+
mgr.set('API_KEY', 'sk-abc123');
|
|
20
|
+
expect(mgr.get('API_KEY')).toBe('sk-abc123');
|
|
21
|
+
expect(existsSync(fp)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should persist and reload secrets', () => {
|
|
25
|
+
const fp = testPath();
|
|
26
|
+
const mgr1 = new SecretsManager({ password: 'pw', filePath: fp });
|
|
27
|
+
mgr1.set('TOKEN', 'xyz');
|
|
28
|
+
const mgr2 = new SecretsManager({ password: 'pw', filePath: fp });
|
|
29
|
+
expect(mgr2.get('TOKEN')).toBe('xyz');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should fail with wrong password', () => {
|
|
33
|
+
const fp = testPath();
|
|
34
|
+
const mgr = new SecretsManager({ password: 'right', filePath: fp });
|
|
35
|
+
mgr.set('KEY', 'val');
|
|
36
|
+
expect(() => new SecretsManager({ password: 'wrong', filePath: fp })).toThrow();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should delete secrets', () => {
|
|
40
|
+
const fp = testPath();
|
|
41
|
+
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
42
|
+
mgr.set('A', '1');
|
|
43
|
+
expect(mgr.delete('A')).toBe(true);
|
|
44
|
+
expect(mgr.get('A')).toBeUndefined();
|
|
45
|
+
expect(mgr.delete('nonexistent')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should list secret keys', () => {
|
|
49
|
+
const fp = testPath();
|
|
50
|
+
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
51
|
+
mgr.set('A', '1');
|
|
52
|
+
mgr.set('B', '2');
|
|
53
|
+
expect(mgr.list().sort()).toEqual(['A', 'B']);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should check has', () => {
|
|
57
|
+
const fp = testPath();
|
|
58
|
+
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
59
|
+
mgr.set('X', 'y');
|
|
60
|
+
expect(mgr.has('X')).toBe(true);
|
|
61
|
+
expect(mgr.has('Z')).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should inject secrets into env object', () => {
|
|
65
|
+
const fp = testPath();
|
|
66
|
+
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
67
|
+
mgr.set('DB_HOST', 'localhost');
|
|
68
|
+
mgr.set('DB_PASS', 'secret');
|
|
69
|
+
const env: Record<string, string | undefined> = { PATH: '/usr/bin' };
|
|
70
|
+
mgr.inject(env, ['DB_HOST']);
|
|
71
|
+
expect(env.DB_HOST).toBe('localhost');
|
|
72
|
+
expect(env.DB_PASS).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should inject all secrets when no keys specified', () => {
|
|
76
|
+
const fp = testPath();
|
|
77
|
+
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
78
|
+
mgr.set('A', '1');
|
|
79
|
+
mgr.set('B', '2');
|
|
80
|
+
const env: Record<string, string | undefined> = {};
|
|
81
|
+
mgr.inject(env);
|
|
82
|
+
expect(env.A).toBe('1');
|
|
83
|
+
expect(env.B).toBe('2');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should rotate password', () => {
|
|
87
|
+
const fp = testPath();
|
|
88
|
+
const mgr = new SecretsManager({ password: 'old', filePath: fp });
|
|
89
|
+
mgr.set('KEY', 'value');
|
|
90
|
+
mgr.rotate('new');
|
|
91
|
+
// Old password should fail
|
|
92
|
+
expect(() => new SecretsManager({ password: 'old', filePath: fp })).toThrow();
|
|
93
|
+
// New password should work
|
|
94
|
+
const mgr2 = new SecretsManager({ password: 'new', filePath: fp });
|
|
95
|
+
expect(mgr2.get('KEY')).toBe('value');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should export and import', () => {
|
|
99
|
+
const fp1 = testPath();
|
|
100
|
+
const mgr = new SecretsManager({ password: 'pw', filePath: fp1 });
|
|
101
|
+
mgr.set('SECRET', 'data');
|
|
102
|
+
const exported = mgr.exportEncrypted();
|
|
103
|
+
const fp2 = testPath();
|
|
104
|
+
const mgr2 = SecretsManager.importEncrypted(exported, 'pw', fp2);
|
|
105
|
+
expect(mgr2.get('SECRET')).toBe('data');
|
|
106
|
+
});
|
|
107
|
+
});
|