heyio 0.42.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -52
- package/dist/api/auth.js +35 -38
- package/dist/api/server.js +157 -1139
- package/dist/config.js +49 -32
- package/dist/copilot/agents.js +72 -1055
- package/dist/copilot/client.js +6 -17
- package/dist/copilot/io-scheduler.js +55 -139
- package/dist/copilot/model-router.js +100 -72
- package/dist/copilot/orchestrator.js +91 -515
- package/dist/copilot/scheduler.js +67 -189
- package/dist/copilot/skills.js +41 -366
- package/dist/copilot/system-message.js +40 -200
- package/dist/copilot/tools.js +191 -2042
- package/dist/daemon.js +54 -201
- package/dist/index.js +15 -133
- package/dist/mcp/config.js +23 -31
- package/dist/mcp/index.js +2 -3
- package/dist/mcp/registry.js +33 -88
- package/dist/notify.js +18 -100
- package/dist/paths.js +13 -24
- package/dist/setup.js +35 -0
- package/dist/store/db.js +111 -297
- package/dist/store/feed.js +29 -97
- package/dist/store/instances.js +56 -121
- package/dist/store/schedules.js +21 -73
- package/dist/store/squads.js +35 -186
- package/dist/store/tasks.js +25 -168
- package/dist/telegram/bot.js +20 -312
- package/dist/telegram/handlers.js +39 -3
- package/dist/watchdog.js +31 -45
- package/dist/wiki/fs.js +38 -155
- package/dist/wiki/search.js +31 -44
- package/package.json +5 -8
- package/web-dist/assets/ChatView-EFFiln1H.js +11 -0
- package/web-dist/assets/FeedView-bN4NMOL7.js +6 -0
- package/web-dist/assets/LoginView-CNtasq3n.js +1 -0
- package/web-dist/assets/McpView-C2CHiwsi.js +1 -0
- package/web-dist/assets/SchedulesView-CyilLban.js +1 -0
- package/web-dist/assets/SettingsView-1wLXKEF4.js +1 -0
- package/web-dist/assets/SkillsView-BLsD-0u0.js +1 -0
- package/web-dist/assets/SquadDetailView-CsCw2ZLp.js +21 -0
- package/web-dist/assets/SquadsView-DQ3vFlyO.js +6 -0
- package/web-dist/assets/WikiView-19M3oqnq.js +21 -0
- package/web-dist/assets/api-WGvTsXaE.js +1 -0
- package/web-dist/assets/index-D7M5O-_l.css +1 -0
- package/web-dist/assets/index-DZOS9syn.js +95 -0
- package/web-dist/assets/plus-BOvyX1BC.js +6 -0
- package/web-dist/assets/trash-2-DHoetkC4.js +6 -0
- package/web-dist/favicon.svg +4 -1
- package/web-dist/index.html +7 -10
- package/dist/api/logout.test.js +0 -129
- package/dist/api/mcp.test.js +0 -285
- package/dist/api/wiki.test.js +0 -283
- package/dist/auth/session-logic.js +0 -79
- package/dist/auth/session-logic.test.js +0 -201
- package/dist/copilot/auto-complete-instance.test.js +0 -104
- package/dist/copilot/cron.js +0 -136
- package/dist/copilot/event-summary.js +0 -286
- package/dist/copilot/instance-deactivate.test.js +0 -119
- package/dist/copilot/model-router.test.js +0 -71
- package/dist/copilot/review-backfill.js +0 -57
- package/dist/copilot/session-timeout.js +0 -112
- package/dist/copilot/session-timeout.test.js +0 -372
- package/dist/copilot/skills.test.js +0 -55
- package/dist/copilot/universes.js +0 -469
- package/dist/instance-watchdog.js +0 -104
- package/dist/instance-watchdog.test.js +0 -183
- package/dist/mcp/client.js +0 -109
- package/dist/mcp/client.test.js +0 -99
- package/dist/mcp/config.test.js +0 -49
- package/dist/mcp/registry.test.js +0 -79
- package/dist/notify.test.js +0 -232
- package/dist/store/feed.test.js +0 -279
- package/dist/store/instances.test.js +0 -310
- package/dist/store/io-schedules.js +0 -63
- package/dist/store/notifications.js +0 -79
- package/dist/store/notifications.test.js +0 -197
- package/dist/store/schedule-runs.js +0 -46
- package/dist/store/squads.test.js +0 -405
- package/dist/store/tasks.test.js +0 -150
- package/dist/store/worktrees.js +0 -83
- package/dist/tui/index.js +0 -286
- package/dist/update.js +0 -81
- package/dist/watchdog.test.js +0 -83
- package/dist/wiki/wiki-squad.test.js +0 -54
- package/web-dist/assets/AgentActivityView-CedxxE6K.js +0 -1
- package/web-dist/assets/ChatView-DMkYQo_V.js +0 -4
- package/web-dist/assets/FeedView-BH4q-31V.js +0 -1
- package/web-dist/assets/InboxView-BVwVP4EW.js +0 -1
- package/web-dist/assets/LoginView-DRPDhnwu.js +0 -1
- package/web-dist/assets/McpView-D8yWz-lq.js +0 -1
- package/web-dist/assets/SchedulesView-BzzyncGF.js +0 -1
- package/web-dist/assets/SettingsTabs.vue_vue_type_script_setup_true_lang-oW3ySu7Y.js +0 -1
- package/web-dist/assets/SkillsView-oxpYuhx7.js +0 -1
- package/web-dist/assets/SquadsView-CaKUIKlq.js +0 -1
- package/web-dist/assets/StatusIndicator.vue_vue_type_script_setup_true_lang-8U15Qp_Q.js +0 -1
- package/web-dist/assets/WikiView-C5jXUlfW.js +0 -1
- package/web-dist/assets/index-BrWzNw-N.css +0 -10
- package/web-dist/assets/index-f67odrrt.js +0 -81
- package/web-dist/icons.svg +0 -24
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth session handling regression tests — Supabase JS v2 integration decisions.
|
|
3
|
-
*
|
|
4
|
-
* These tests guard the behavioral decisions made across four consecutive auth
|
|
5
|
-
* patches (#189→#192→#194→#196) that fixed undocumented Supabase JS v2 side-
|
|
6
|
-
* effects. They test the logic extracted into src/auth/session-logic.ts, which
|
|
7
|
-
* mirrors the implementation in web/src/stores/auth.ts.
|
|
8
|
-
*
|
|
9
|
-
* If auth store logic changes, update session-logic.ts to match AND verify
|
|
10
|
-
* these tests still pass.
|
|
11
|
-
*/
|
|
12
|
-
import { describe, it } from "node:test";
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { handleAuthStateChange, getAccessToken, } from "./session-logic.js";
|
|
15
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
16
|
-
function makeSession(overrides = {}) {
|
|
17
|
-
return {
|
|
18
|
-
access_token: "tok-abc",
|
|
19
|
-
expires_at: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
|
20
|
-
user: { id: "user-1" },
|
|
21
|
-
...overrides,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
const neverCalled = async () => {
|
|
25
|
-
throw new Error("refresh should not have been called");
|
|
26
|
-
};
|
|
27
|
-
// Use a fixed epoch (multiple of 1000ms) for deterministic boundary tests.
|
|
28
|
-
// T = 1_000_000_000_000ms (epoch seconds = 1_000_000_000)
|
|
29
|
-
const FIXED_NOW_MS = 1_000_000_000_000;
|
|
30
|
-
const FIXED_NOW_S = FIXED_NOW_MS / 1000; // 1_000_000_000
|
|
31
|
-
// ── onAuthStateChange session caching ────────────────────────────────────────
|
|
32
|
-
describe("handleAuthStateChange — onAuthStateChange caching rules", () => {
|
|
33
|
-
it("accepts a valid session on SIGNED_IN", () => {
|
|
34
|
-
const s = makeSession();
|
|
35
|
-
const result = handleAuthStateChange(null, "SIGNED_IN", s);
|
|
36
|
-
assert.deepEqual(result, s);
|
|
37
|
-
});
|
|
38
|
-
it("accepts a valid session on INITIAL_SESSION", () => {
|
|
39
|
-
const s = makeSession();
|
|
40
|
-
const result = handleAuthStateChange(null, "INITIAL_SESSION", s);
|
|
41
|
-
assert.deepEqual(result, s);
|
|
42
|
-
});
|
|
43
|
-
it("accepts a valid session on TOKEN_REFRESHED", () => {
|
|
44
|
-
const old = makeSession({ access_token: "tok-old" });
|
|
45
|
-
const fresh = makeSession({ access_token: "tok-new" });
|
|
46
|
-
const result = handleAuthStateChange(old, "TOKEN_REFRESHED", fresh);
|
|
47
|
-
assert.equal(result?.access_token, "tok-new");
|
|
48
|
-
});
|
|
49
|
-
// Critical regression: SIGNED_OUT is the ONLY event that clears the cache.
|
|
50
|
-
it("clears session on SIGNED_OUT with null session", () => {
|
|
51
|
-
const s = makeSession();
|
|
52
|
-
const result = handleAuthStateChange(s, "SIGNED_OUT", null);
|
|
53
|
-
assert.equal(result, null);
|
|
54
|
-
});
|
|
55
|
-
// Critical regression: internal null-session events must NOT poison the cache.
|
|
56
|
-
it("does NOT clear session on TOKEN_REFRESHED with null — keeps existing cache", () => {
|
|
57
|
-
const cached = makeSession({ access_token: "tok-valid" });
|
|
58
|
-
const result = handleAuthStateChange(cached, "TOKEN_REFRESHED", null);
|
|
59
|
-
assert.equal(result?.access_token, "tok-valid");
|
|
60
|
-
});
|
|
61
|
-
it("does NOT clear session on INITIAL_SESSION with null — keeps existing cache", () => {
|
|
62
|
-
const cached = makeSession({ access_token: "tok-valid" });
|
|
63
|
-
const result = handleAuthStateChange(cached, "INITIAL_SESSION", null);
|
|
64
|
-
assert.equal(result?.access_token, "tok-valid");
|
|
65
|
-
});
|
|
66
|
-
it("does NOT clear session on USER_UPDATED with null — keeps existing cache", () => {
|
|
67
|
-
const cached = makeSession({ access_token: "tok-valid" });
|
|
68
|
-
const result = handleAuthStateChange(cached, "USER_UPDATED", null);
|
|
69
|
-
assert.equal(result?.access_token, "tok-valid");
|
|
70
|
-
});
|
|
71
|
-
it("returns null when cache is already null and null-session event fires", () => {
|
|
72
|
-
const result = handleAuthStateChange(null, "TOKEN_REFRESHED", null);
|
|
73
|
-
assert.equal(result, null);
|
|
74
|
-
});
|
|
75
|
-
it("null-session SIGNED_OUT with no prior cache stays null", () => {
|
|
76
|
-
const result = handleAuthStateChange(null, "SIGNED_OUT", null);
|
|
77
|
-
assert.equal(result, null);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
// ── getAccessToken — cached session path ─────────────────────────────────────
|
|
81
|
-
describe("getAccessToken — reads from cached session", () => {
|
|
82
|
-
it("returns cached access token when session is valid and not near expiry", async () => {
|
|
83
|
-
const session = makeSession({ access_token: "tok-valid" });
|
|
84
|
-
const token = await getAccessToken(session, true, neverCalled);
|
|
85
|
-
assert.equal(token, "tok-valid");
|
|
86
|
-
});
|
|
87
|
-
it("returns null when auth is disabled and cache is null", async () => {
|
|
88
|
-
const token = await getAccessToken(null, false, neverCalled);
|
|
89
|
-
assert.equal(token, null);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
// ── getAccessToken — proactive refresh near expiry ───────────────────────────
|
|
93
|
-
describe("getAccessToken — proactive refresh within 30s of expiry", () => {
|
|
94
|
-
it("calls refresh and returns new token when within 30s of expiry", async () => {
|
|
95
|
-
// Session expires in 25 seconds (within 30s threshold)
|
|
96
|
-
const session = makeSession({
|
|
97
|
-
access_token: "tok-expiring",
|
|
98
|
-
expires_at: Math.floor(Date.now() / 1000) + 25,
|
|
99
|
-
});
|
|
100
|
-
const freshSession = makeSession({ access_token: "tok-fresh" });
|
|
101
|
-
let refreshCalled = false;
|
|
102
|
-
const refresh = async () => { refreshCalled = true; return freshSession; };
|
|
103
|
-
const token = await getAccessToken(session, true, refresh);
|
|
104
|
-
assert.equal(refreshCalled, true);
|
|
105
|
-
assert.equal(token, "tok-fresh");
|
|
106
|
-
});
|
|
107
|
-
it("does NOT call refresh when expiry is more than 30s away", async () => {
|
|
108
|
-
const session = makeSession({
|
|
109
|
-
access_token: "tok-valid",
|
|
110
|
-
expires_at: Math.floor(Date.now() / 1000) + 60, // 60s away
|
|
111
|
-
});
|
|
112
|
-
let refreshCalled = false;
|
|
113
|
-
const refresh = async () => { refreshCalled = true; return null; };
|
|
114
|
-
const token = await getAccessToken(session, true, refresh);
|
|
115
|
-
assert.equal(refreshCalled, false);
|
|
116
|
-
assert.equal(token, "tok-valid");
|
|
117
|
-
});
|
|
118
|
-
it("falls back to cached token if refresh throws near expiry", async () => {
|
|
119
|
-
const session = makeSession({
|
|
120
|
-
access_token: "tok-expiring",
|
|
121
|
-
expires_at: Math.floor(Date.now() / 1000) + 10,
|
|
122
|
-
});
|
|
123
|
-
const refresh = async () => { throw new Error("network error"); };
|
|
124
|
-
const token = await getAccessToken(session, true, refresh);
|
|
125
|
-
assert.equal(token, "tok-expiring");
|
|
126
|
-
});
|
|
127
|
-
it("falls back to cached token if refresh returns null near expiry", async () => {
|
|
128
|
-
const session = makeSession({
|
|
129
|
-
access_token: "tok-expiring",
|
|
130
|
-
expires_at: Math.floor(Date.now() / 1000) + 10,
|
|
131
|
-
});
|
|
132
|
-
const refresh = async () => null;
|
|
133
|
-
const token = await getAccessToken(session, true, refresh);
|
|
134
|
-
assert.equal(token, "tok-expiring");
|
|
135
|
-
});
|
|
136
|
-
// Boundary: expires_at * 1000 - now == 30_000 is NOT < 30_000, so no refresh.
|
|
137
|
-
// Use FIXED_NOW_MS (exact multiple of 1000) to avoid floor-truncation artifacts.
|
|
138
|
-
it("does NOT refresh when exactly 30s remain (boundary is exclusive)", async () => {
|
|
139
|
-
const session = makeSession({
|
|
140
|
-
access_token: "tok-boundary",
|
|
141
|
-
expires_at: FIXED_NOW_S + 30, // exactly 30_000ms away from FIXED_NOW_MS
|
|
142
|
-
});
|
|
143
|
-
let refreshCalled = false;
|
|
144
|
-
const refresh = async () => { refreshCalled = true; return null; };
|
|
145
|
-
const token = await getAccessToken(session, true, refresh, () => FIXED_NOW_MS);
|
|
146
|
-
assert.equal(refreshCalled, false);
|
|
147
|
-
assert.equal(token, "tok-boundary");
|
|
148
|
-
});
|
|
149
|
-
// Boundary: expires_at * 1000 - now == 29_000 is < 30_000, so refresh fires.
|
|
150
|
-
it("refreshes when 29s remain (1s inside the 30s window)", async () => {
|
|
151
|
-
const freshSession = makeSession({ access_token: "tok-refreshed" });
|
|
152
|
-
const session = makeSession({
|
|
153
|
-
access_token: "tok-boundary",
|
|
154
|
-
expires_at: FIXED_NOW_S + 29, // 29_000ms away — within threshold
|
|
155
|
-
});
|
|
156
|
-
let refreshCalled = false;
|
|
157
|
-
const refresh = async () => { refreshCalled = true; return freshSession; };
|
|
158
|
-
const token = await getAccessToken(session, true, refresh, () => FIXED_NOW_MS);
|
|
159
|
-
assert.equal(refreshCalled, true);
|
|
160
|
-
assert.equal(token, "tok-refreshed");
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
// ── getAccessToken — recovery fallback when cache is null ────────────────────
|
|
164
|
-
describe("getAccessToken — recovery fallback when cache is null", () => {
|
|
165
|
-
it("attempts refreshSession() when cache is null and auth is enabled", async () => {
|
|
166
|
-
const freshSession = makeSession({ access_token: "tok-recovered" });
|
|
167
|
-
let refreshCalled = false;
|
|
168
|
-
const refresh = async () => { refreshCalled = true; return freshSession; };
|
|
169
|
-
const token = await getAccessToken(null, true, refresh);
|
|
170
|
-
assert.equal(refreshCalled, true);
|
|
171
|
-
assert.equal(token, "tok-recovered");
|
|
172
|
-
});
|
|
173
|
-
it("returns null when auth is enabled but refreshSession() returns null", async () => {
|
|
174
|
-
const token = await getAccessToken(null, true, async () => null);
|
|
175
|
-
assert.equal(token, null);
|
|
176
|
-
});
|
|
177
|
-
it("returns null when auth is enabled but refreshSession() throws", async () => {
|
|
178
|
-
const refresh = async () => { throw new Error("refresh failed"); };
|
|
179
|
-
const token = await getAccessToken(null, true, refresh);
|
|
180
|
-
assert.equal(token, null);
|
|
181
|
-
});
|
|
182
|
-
it("does NOT call refreshSession() when auth is disabled", async () => {
|
|
183
|
-
let refreshCalled = false;
|
|
184
|
-
const refresh = async () => { refreshCalled = true; return null; };
|
|
185
|
-
const token = await getAccessToken(null, false, refresh);
|
|
186
|
-
assert.equal(refreshCalled, false);
|
|
187
|
-
assert.equal(token, null);
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
// ── getAccessToken — session without expires_at ───────────────────────────────
|
|
191
|
-
describe("getAccessToken — session without expires_at", () => {
|
|
192
|
-
it("returns cached token without attempting refresh if expires_at is undefined", async () => {
|
|
193
|
-
const session = { access_token: "tok-no-expiry" };
|
|
194
|
-
let refreshCalled = false;
|
|
195
|
-
const refresh = async () => { refreshCalled = true; return null; };
|
|
196
|
-
const token = await getAccessToken(session, true, refresh);
|
|
197
|
-
assert.equal(refreshCalled, false);
|
|
198
|
-
assert.equal(token, "tok-no-expiry");
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
//# sourceMappingURL=session-logic.test.js.map
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, after } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { setDbPathForTests, closeDb, getDb } from "../store/db.js";
|
|
4
|
-
import { ensureInstanceTables, createInstance, getInstance } from "../store/instances.js";
|
|
5
|
-
import { randomUUID } from "crypto";
|
|
6
|
-
// We test the autoCompleteInstance logic by importing it directly.
|
|
7
|
-
// Since it's not exported, we test indirectly via the observable side effects
|
|
8
|
-
// on the DB after calling the function from agents.ts.
|
|
9
|
-
// For unit testing, we extract the logic into a testable helper.
|
|
10
|
-
// Actually, let's test the exported behavior by simulating what agents.ts does:
|
|
11
|
-
// import the function via a re-export or test the DB state.
|
|
12
|
-
// The function is module-private in agents.ts. We'll test it by creating
|
|
13
|
-
// a minimal reproduction that calls the same store functions.
|
|
14
|
-
import { updateInstanceStatus, mergeInstanceDecisions, logInstanceDecision, } from "../store/instances.js";
|
|
15
|
-
import { createFeedEntry, listFeedEntries } from "../store/feed.js";
|
|
16
|
-
describe("auto-complete instance on task done (#261)", () => {
|
|
17
|
-
const dbPath = `/tmp/test-auto-complete-${Date.now()}.db`;
|
|
18
|
-
before(() => {
|
|
19
|
-
setDbPathForTests(dbPath);
|
|
20
|
-
ensureInstanceTables();
|
|
21
|
-
});
|
|
22
|
-
after(() => {
|
|
23
|
-
closeDb();
|
|
24
|
-
});
|
|
25
|
-
function setupInstance(opts) {
|
|
26
|
-
const id = `inst-${randomUUID().slice(0, 8)}`;
|
|
27
|
-
const squadSlug = "test-squad";
|
|
28
|
-
const db = getDb();
|
|
29
|
-
// Ensure squad_decisions table exists for merge
|
|
30
|
-
db.exec(`CREATE TABLE IF NOT EXISTS squad_decisions (
|
|
31
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
32
|
-
squad_slug TEXT NOT NULL,
|
|
33
|
-
decision TEXT NOT NULL,
|
|
34
|
-
context TEXT,
|
|
35
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
36
|
-
)`);
|
|
37
|
-
createInstance({
|
|
38
|
-
id,
|
|
39
|
-
masterSquadSlug: squadSlug,
|
|
40
|
-
worktreePath: `/tmp/fake-worktree-${id}`,
|
|
41
|
-
branchName: `instance/${id}`,
|
|
42
|
-
});
|
|
43
|
-
if (opts?.status) {
|
|
44
|
-
updateInstanceStatus(id, opts.status);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
updateInstanceStatus(id, "active");
|
|
48
|
-
}
|
|
49
|
-
return { id, squadSlug };
|
|
50
|
-
}
|
|
51
|
-
it("auto-completes an active instance when task with instance_id finishes", () => {
|
|
52
|
-
const { id, squadSlug } = setupInstance();
|
|
53
|
-
logInstanceDecision(id, "test decision", "test context");
|
|
54
|
-
// Simulate what autoCompleteInstance does
|
|
55
|
-
const instance = getInstance(id);
|
|
56
|
-
assert.ok(instance);
|
|
57
|
-
assert.equal(instance.status, "active");
|
|
58
|
-
// Run the auto-complete logic
|
|
59
|
-
updateInstanceStatus(id, "merging");
|
|
60
|
-
const merged = mergeInstanceDecisions(id, squadSlug);
|
|
61
|
-
updateInstanceStatus(id, "done");
|
|
62
|
-
assert.equal(merged, 1);
|
|
63
|
-
const completed = getInstance(id);
|
|
64
|
-
assert.equal(completed.status, "done");
|
|
65
|
-
assert.ok(completed.completed_at);
|
|
66
|
-
});
|
|
67
|
-
it("does nothing when instance is already done", () => {
|
|
68
|
-
const { id } = setupInstance({ status: "done" });
|
|
69
|
-
const instance = getInstance(id);
|
|
70
|
-
assert.equal(instance.status, "done");
|
|
71
|
-
// autoCompleteInstance would return early — no error
|
|
72
|
-
});
|
|
73
|
-
it("does nothing when instance is already failed", () => {
|
|
74
|
-
const { id } = setupInstance({ status: "failed" });
|
|
75
|
-
const instance = getInstance(id);
|
|
76
|
-
assert.equal(instance.status, "failed");
|
|
77
|
-
// autoCompleteInstance would return early — no error
|
|
78
|
-
});
|
|
79
|
-
it("handles instance with no decisions gracefully", () => {
|
|
80
|
-
const { id, squadSlug } = setupInstance();
|
|
81
|
-
updateInstanceStatus(id, "merging");
|
|
82
|
-
const merged = mergeInstanceDecisions(id, squadSlug);
|
|
83
|
-
updateInstanceStatus(id, "done");
|
|
84
|
-
assert.equal(merged, 0);
|
|
85
|
-
const completed = getInstance(id);
|
|
86
|
-
assert.equal(completed.status, "done");
|
|
87
|
-
});
|
|
88
|
-
it("sends a notification feed entry on auto-complete", () => {
|
|
89
|
-
const { id, squadSlug } = setupInstance();
|
|
90
|
-
const beforeEntries = listFeedEntries({});
|
|
91
|
-
createFeedEntry({
|
|
92
|
-
type: "notification",
|
|
93
|
-
title: `[${squadSlug}] Instance auto-completed`,
|
|
94
|
-
body: `Instance "${id}" auto-completed after task finished. 0 decision(s) merged to master squad.`,
|
|
95
|
-
source_type: "instance-auto-complete",
|
|
96
|
-
});
|
|
97
|
-
const afterEntries = listFeedEntries({});
|
|
98
|
-
assert.equal(afterEntries.length, beforeEntries.length + 1);
|
|
99
|
-
const latest = afterEntries[0];
|
|
100
|
-
assert.ok(latest.title.includes("auto-completed"));
|
|
101
|
-
assert.ok(latest.body.includes(id));
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
//# sourceMappingURL=auto-complete-instance.test.js.map
|
package/dist/copilot/cron.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
// Minimal standard 5-field cron parser + next-run calculator.
|
|
2
|
-
//
|
|
3
|
-
// Fields (in order): minute, hour, day-of-month, month, day-of-week.
|
|
4
|
-
// Supported syntax per field:
|
|
5
|
-
// * — all values
|
|
6
|
-
// N — single value
|
|
7
|
-
// A,B,C — list
|
|
8
|
-
// A-B — range
|
|
9
|
-
// A-B/N or */N — step (A-B/N restricts to range; */N applies to full range)
|
|
10
|
-
//
|
|
11
|
-
// Day-of-week: 0 or 7 = Sunday, 1 = Monday … (standard Unix cron).
|
|
12
|
-
//
|
|
13
|
-
// Matching semantics: when both day-of-month and day-of-week are restricted
|
|
14
|
-
// (i.e. neither is "*"), Vixie cron uses an OR between them — we follow that.
|
|
15
|
-
const FIELD_RANGES = [
|
|
16
|
-
[0, 59],
|
|
17
|
-
[0, 23],
|
|
18
|
-
[1, 31],
|
|
19
|
-
[1, 12],
|
|
20
|
-
[0, 7],
|
|
21
|
-
];
|
|
22
|
-
function parseField(raw, [min, max]) {
|
|
23
|
-
const out = new Set();
|
|
24
|
-
for (const part of raw.split(",")) {
|
|
25
|
-
const stepMatch = part.match(/^(.+?)\/(\d+)$/);
|
|
26
|
-
const body = stepMatch ? stepMatch[1] : part;
|
|
27
|
-
const step = stepMatch ? parseInt(stepMatch[2], 10) : 1;
|
|
28
|
-
if (!(step >= 1))
|
|
29
|
-
throw new Error(`Invalid step in cron field: "${part}"`);
|
|
30
|
-
let lo, hi;
|
|
31
|
-
if (body === "*") {
|
|
32
|
-
lo = min;
|
|
33
|
-
hi = max;
|
|
34
|
-
}
|
|
35
|
-
else if (body.includes("-")) {
|
|
36
|
-
const [a, b] = body.split("-").map((n) => parseInt(n, 10));
|
|
37
|
-
if (Number.isNaN(a) || Number.isNaN(b)) {
|
|
38
|
-
throw new Error(`Invalid range in cron field: "${part}"`);
|
|
39
|
-
}
|
|
40
|
-
lo = a;
|
|
41
|
-
hi = b;
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
const v = parseInt(body, 10);
|
|
45
|
-
if (Number.isNaN(v))
|
|
46
|
-
throw new Error(`Invalid cron field value: "${part}"`);
|
|
47
|
-
lo = v;
|
|
48
|
-
hi = v;
|
|
49
|
-
}
|
|
50
|
-
if (lo < min || hi > max || lo > hi) {
|
|
51
|
-
throw new Error(`Cron field out of range [${min}-${max}]: "${part}"`);
|
|
52
|
-
}
|
|
53
|
-
for (let v = lo; v <= hi; v += step)
|
|
54
|
-
out.add(v);
|
|
55
|
-
}
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
export function parseCron(expr) {
|
|
59
|
-
const trimmed = expr.trim().replace(/\s+/g, " ");
|
|
60
|
-
const parts = trimmed.split(" ");
|
|
61
|
-
if (parts.length !== 5) {
|
|
62
|
-
throw new Error(`Cron expression must have 5 fields (minute hour dom month dow), got ${parts.length}: "${expr}"`);
|
|
63
|
-
}
|
|
64
|
-
const [mRaw, hRaw, domRaw, monRaw, dowRaw] = parts;
|
|
65
|
-
const minutes = parseField(mRaw, FIELD_RANGES[0]);
|
|
66
|
-
const hours = parseField(hRaw, FIELD_RANGES[1]);
|
|
67
|
-
const doms = parseField(domRaw, FIELD_RANGES[2]);
|
|
68
|
-
const months = parseField(monRaw, FIELD_RANGES[3]);
|
|
69
|
-
const dowsRaw = parseField(dowRaw, FIELD_RANGES[4]);
|
|
70
|
-
const dows = new Set();
|
|
71
|
-
for (const v of dowsRaw)
|
|
72
|
-
dows.add(v === 7 ? 0 : v);
|
|
73
|
-
return {
|
|
74
|
-
minutes,
|
|
75
|
-
hours,
|
|
76
|
-
doms,
|
|
77
|
-
months,
|
|
78
|
-
dows,
|
|
79
|
-
domStar: domRaw === "*",
|
|
80
|
-
dowStar: dowRaw === "*",
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Return the next Date strictly after `after` that matches the cron expression.
|
|
85
|
-
* Iterates minute-by-minute with month/day fast-forwarding. Capped at ~5
|
|
86
|
-
* years lookahead to guard against unsatisfiable expressions.
|
|
87
|
-
*/
|
|
88
|
-
export function nextRun(expr, after = new Date()) {
|
|
89
|
-
const c = typeof expr === "string" ? parseCron(expr) : expr;
|
|
90
|
-
const cursor = new Date(after.getTime());
|
|
91
|
-
cursor.setSeconds(0, 0);
|
|
92
|
-
cursor.setMinutes(cursor.getMinutes() + 1);
|
|
93
|
-
const limit = new Date(after.getTime() + 5 * 366 * 24 * 60 * 60 * 1000);
|
|
94
|
-
while (cursor <= limit) {
|
|
95
|
-
const month = cursor.getMonth() + 1;
|
|
96
|
-
if (!c.months.has(month)) {
|
|
97
|
-
cursor.setMonth(cursor.getMonth() + 1, 1);
|
|
98
|
-
cursor.setHours(0, 0, 0, 0);
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
const dom = cursor.getDate();
|
|
102
|
-
const dow = cursor.getDay();
|
|
103
|
-
const dayOk = c.domStar && c.dowStar
|
|
104
|
-
? true
|
|
105
|
-
: c.domStar
|
|
106
|
-
? c.dows.has(dow)
|
|
107
|
-
: c.dowStar
|
|
108
|
-
? c.doms.has(dom)
|
|
109
|
-
: c.doms.has(dom) || c.dows.has(dow);
|
|
110
|
-
if (!dayOk) {
|
|
111
|
-
cursor.setDate(cursor.getDate() + 1);
|
|
112
|
-
cursor.setHours(0, 0, 0, 0);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (!c.hours.has(cursor.getHours())) {
|
|
116
|
-
cursor.setHours(cursor.getHours() + 1, 0, 0, 0);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (!c.minutes.has(cursor.getMinutes())) {
|
|
120
|
-
cursor.setMinutes(cursor.getMinutes() + 1, 0, 0);
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
return cursor;
|
|
124
|
-
}
|
|
125
|
-
throw new Error(`No next run found within 5 years for cron expression`);
|
|
126
|
-
}
|
|
127
|
-
export function validateCron(expr) {
|
|
128
|
-
try {
|
|
129
|
-
const next = nextRun(expr);
|
|
130
|
-
return { ok: true, next };
|
|
131
|
-
}
|
|
132
|
-
catch (err) {
|
|
133
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
//# sourceMappingURL=cron.js.map
|