@app-vitals/agent-runtime 0.0.1 → 0.1.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/Dockerfile +265 -0
- package/agent-account-status-ref.ts +53 -0
- package/agent-account-status-ref.unit.test.ts +24 -0
- package/agent-extension.ts +144 -0
- package/agent-extension.unit.test.ts +139 -0
- package/agent-repos-ref.ts +63 -0
- package/agent-repos-ref.unit.test.ts +84 -0
- package/agent-slack-membership-ref.ts +94 -0
- package/agent-slack-membership-ref.unit.test.ts +201 -0
- package/agent-trial-expiry-ref.ts +63 -0
- package/agent-trial-expiry-ref.unit.test.ts +84 -0
- package/anthropic-federation-guard.ts +72 -0
- package/anthropic-federation-guard.unit.test.ts +127 -0
- package/base-image-contract.smoke.test.ts +149 -0
- package/boot.ts +845 -0
- package/browser.integration.test.ts +52 -0
- package/browser.ts +34 -0
- package/chat-poller.integration.test.ts +109 -0
- package/chat-poller.ts +401 -0
- package/chat-poller.unit.test.ts +1581 -0
- package/chat-token-reporter.integration.test.ts +282 -0
- package/chat-token-reporter.ts +136 -0
- package/claude-version.ts +55 -0
- package/claude-version.unit.test.ts +73 -0
- package/claude.ts +1054 -0
- package/claude.unit.test.ts +3042 -0
- package/cleanup-after-days-ref.ts +83 -0
- package/cleanup-after-days-ref.unit.test.ts +89 -0
- package/cleanup-merged-worktrees-ref.ts +75 -0
- package/cleanup-merged-worktrees-ref.unit.test.ts +72 -0
- package/cli-args.ts +36 -0
- package/cli-args.unit.test.ts +94 -0
- package/clock.ts +47 -0
- package/config.ts +99 -0
- package/config.unit.test.ts +356 -0
- package/context-stamp.ts +189 -0
- package/context-stamp.unit.test.ts +159 -0
- package/cron-failure-reporter.ts +107 -0
- package/cron-failure-reporter.unit.test.ts +244 -0
- package/cron-handler.integration.test.ts +614 -0
- package/cron-handler.smoke.test.ts +1371 -0
- package/cron-handler.ts +750 -0
- package/cron-handler.unit.test.ts +1177 -0
- package/cron-run-reporter.integration.test.ts +806 -0
- package/cron-run-reporter.ts +398 -0
- package/crypto.ts +73 -0
- package/cutover-validate.integration.test.ts +245 -0
- package/cutover-validate.ts +84 -0
- package/entrypoint-main.ts +79 -0
- package/entrypoint.integration.test.ts +315 -0
- package/entrypoint.ts +154 -0
- package/errors.ts +61 -0
- package/fixtures/chat-service/attachment.json +15 -0
- package/fixtures/chat-service/messages.json +86 -0
- package/fixtures/chat-service/threads.json +50 -0
- package/fixtures/github-installations/installations.json +1231 -0
- package/fixtures/slack-provision-cassette.json +6 -0
- package/fixtures/specialties/fixture/manifest.yaml +35 -0
- package/fixtures/specialties/fixture/templates/CLAUDE.md.template +7 -0
- package/fixtures/specialties/fixture/templates/IDENTITY.md.template +8 -0
- package/fixtures/specialties/fixture/templates/SOUL.md.template +45 -0
- package/fixtures/stream-json/clean-exit-no-result.ts +53 -0
- package/fixtures/stream-json/clean-multi-turn.ts +209 -0
- package/fixtures/stream-json/progress-phases.ts +203 -0
- package/fixtures/stream-json/skill-attribution.ts +251 -0
- package/fixtures/stream-json/truncated-no-result.ts +84 -0
- package/format.ts +283 -0
- package/format.unit.test.ts +558 -0
- package/gh-helpers.ts +139 -0
- package/gh-helpers.unit.test.ts +253 -0
- package/gh-token-files.integration.test.ts +143 -0
- package/gh-token-files.ts +92 -0
- package/gh-token-files.unit.test.ts +63 -0
- package/github-app-auth.integration.test.ts +419 -0
- package/github-app-auth.ts +193 -0
- package/github-auth-deps.ts +128 -0
- package/github-auth-startup.ts +231 -0
- package/github-auth-startup.unit.test.ts +325 -0
- package/github-installations-manager.integration.test.ts +323 -0
- package/github-installations-reporter.integration.test.ts +124 -0
- package/github-installations-reporter.ts +150 -0
- package/github-installations-reporter.unit.test.ts +166 -0
- package/github-installations.integration.test.ts +88 -0
- package/github-installations.ts +462 -0
- package/github-installations.unit.test.ts +98 -0
- package/health.smoke.test.ts +244 -0
- package/health.ts +193 -0
- package/health.unit.test.ts +40 -0
- package/http-chat-service-client.integration.test.ts +345 -0
- package/http-chat-service-client.ts +287 -0
- package/http-chat-service-client.unit.test.ts +500 -0
- package/log-prefix.ts +12 -0
- package/log-prefix.unit.test.ts +24 -0
- package/markers.ts +178 -0
- package/markers.unit.test.ts +374 -0
- package/package.json +38 -4
- package/piper-voice.ts +116 -0
- package/piper-voice.unit.test.ts +124 -0
- package/pricing.ts +7 -0
- package/pricing.unit.test.ts +149 -0
- package/process-tree-kill.integration.test.ts +133 -0
- package/process-tree-kill.ts +325 -0
- package/process-tree-kill.unit.test.ts +265 -0
- package/progress-milestones.ts +86 -0
- package/progress-milestones.unit.test.ts +188 -0
- package/run-telemetry.ts +241 -0
- package/run-telemetry.unit.test.ts +235 -0
- package/sessions.integration.test.ts +273 -0
- package/sessions.ts +191 -0
- package/sessions.unit.test.ts +94 -0
- package/setup-github-auth.integration.test.ts +858 -0
- package/setup-github-auth.ts +297 -0
- package/setup.fixture.integration.test.ts +109 -0
- package/setup.integration.test.ts +1495 -0
- package/setup.ts +853 -0
- package/setup.unit.test.ts +286 -0
- package/shipwright-config-client.ts +36 -0
- package/shipwright-runtime-client.integration.test.ts +336 -0
- package/shipwright-runtime-client.ts +118 -0
- package/slack-account-status.unit.test.ts +251 -0
- package/slack-bolt-client.integration.test.ts +76 -0
- package/slack-chat-conversation.integration.test.ts +333 -0
- package/slack-progress.ts +510 -0
- package/slack-progress.unit.test.ts +806 -0
- package/slack-socket-mode-ping.integration.test.ts +137 -0
- package/slack-startup.ts +170 -0
- package/slack-startup.unit.test.ts +300 -0
- package/slack-thread-status-tracker.ts +70 -0
- package/slack-thread-status-tracker.unit.test.ts +118 -0
- package/slack-trial-expiry.unit.test.ts +269 -0
- package/slack.integration.test.ts +3979 -0
- package/slack.ts +1286 -0
- package/slack.unit.test.ts +1070 -0
- package/spawn-env.ts +58 -0
- package/spawn-env.unit.test.ts +96 -0
- package/startup-dm.integration.test.ts +156 -0
- package/startup-dm.ts +29 -0
- package/sync-config-clone.ts +238 -0
- package/sync-config-clone.unit.test.ts +429 -0
- package/test-env.ts +27 -0
- package/users.ts +90 -0
- package/users.unit.test.ts +108 -0
- package/voice.integration.test.ts +669 -0
- package/voice.ts +315 -0
- package/voice.unit.test.ts +105 -0
- package/workspace/BOOTSTRAP.md.template +43 -0
- package/workspace/BOOTSTRAP.md.template.content.test.ts +48 -0
- package/workspace/IDENTITY.md.template +8 -0
- package/workspace/RUNTIME.md.template +193 -0
- package/workspace/RUNTIME.md.template.content.test.ts +75 -0
- package/workspace/SOUL.md.template +45 -0
- package/workspace/SPECIALTY.md.template +37 -0
- package/workspace/SPECIALTY.md.template.content.test.ts +50 -0
- package/workspace/VOICE.md.template +8 -0
- package/workspace/mise.toml.template +12 -0
- package/workspace/requirements.txt.template +1 -0
- package/workspace-paths.ts +37 -0
- package/worktree-reaper.ts +244 -0
- package/worktree-reaper.unit.test.ts +380 -0
- package/README.md +0 -3
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/runtime/shipwright-runtime-client.ts
|
|
3
|
+
*
|
|
4
|
+
* Unified runtime client for the three agent-facing Shipwright API methods.
|
|
5
|
+
*
|
|
6
|
+
* - ShipwrightClientError — typed error with statusCode
|
|
7
|
+
* - ShipwrightRuntimeClient — interface for DI
|
|
8
|
+
* - HttpShipwrightRuntimeClient — typed HTTP implementation via openapi-fetch
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
AgentConfigResponse,
|
|
13
|
+
AgentCronJob,
|
|
14
|
+
} from "@app-vitals/agent-admin";
|
|
15
|
+
import type { paths } from "@app-vitals/agent-lib/admin-types";
|
|
16
|
+
import createClient from "openapi-fetch";
|
|
17
|
+
|
|
18
|
+
type FetchFn = (
|
|
19
|
+
url: RequestInfo | URL,
|
|
20
|
+
init?: RequestInit,
|
|
21
|
+
) => Promise<Response>;
|
|
22
|
+
|
|
23
|
+
// ─── Error ────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export class ShipwrightClientError extends Error {
|
|
26
|
+
constructor(
|
|
27
|
+
public readonly statusCode: number,
|
|
28
|
+
message: string,
|
|
29
|
+
) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "ShipwrightClientError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Interface ────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
interface ShipwrightRuntimeClient {
|
|
38
|
+
getAgentConfigBundle(agentId: string): Promise<AgentConfigResponse>;
|
|
39
|
+
listAgentCronJobs(agentId: string): Promise<AgentCronJob[]>;
|
|
40
|
+
reconcileSystemCrons(agentId: string): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── HttpShipwrightRuntimeClient ──────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export class HttpShipwrightRuntimeClient implements ShipwrightRuntimeClient {
|
|
46
|
+
private readonly client: ReturnType<typeof createClient<paths>>;
|
|
47
|
+
private readonly adminClient: ReturnType<typeof createClient<paths>>;
|
|
48
|
+
|
|
49
|
+
constructor(opts: {
|
|
50
|
+
apiUrl: string;
|
|
51
|
+
/** Base URL for admin-tier endpoints (e.g. /agents/:id/...). Defaults to apiUrl when the unified admin service serves both namespaces. */
|
|
52
|
+
adminApiUrl?: string;
|
|
53
|
+
apiKey: string;
|
|
54
|
+
fetchFn?: FetchFn;
|
|
55
|
+
}) {
|
|
56
|
+
const commonOpts = {
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
},
|
|
61
|
+
...(opts.fetchFn ? { fetch: opts.fetchFn } : {}),
|
|
62
|
+
};
|
|
63
|
+
this.client = createClient<paths>({
|
|
64
|
+
baseUrl: opts.apiUrl,
|
|
65
|
+
...commonOpts,
|
|
66
|
+
});
|
|
67
|
+
this.adminClient = createClient<paths>({
|
|
68
|
+
baseUrl: opts.adminApiUrl ?? opts.apiUrl,
|
|
69
|
+
...commonOpts,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async getAgentConfigBundle(agentId: string): Promise<AgentConfigResponse> {
|
|
74
|
+
const { data, error, response } = await this.client.GET(
|
|
75
|
+
"/agents/{id}/config",
|
|
76
|
+
{ params: { path: { id: agentId } } },
|
|
77
|
+
);
|
|
78
|
+
if (error) {
|
|
79
|
+
throw new ShipwrightClientError(
|
|
80
|
+
response.status,
|
|
81
|
+
`GET /agents/${agentId}/config failed: ${response.status}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return data;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async listAgentCronJobs(agentId: string): Promise<AgentCronJob[]> {
|
|
88
|
+
const { data, error, response } = await this.client.GET(
|
|
89
|
+
"/agents/{id}/crons",
|
|
90
|
+
{ params: { path: { id: agentId } } },
|
|
91
|
+
);
|
|
92
|
+
if (error) {
|
|
93
|
+
throw new ShipwrightClientError(
|
|
94
|
+
response.status,
|
|
95
|
+
`GET /agents/${agentId}/crons failed: ${response.status}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
// The API returns ISO date strings; cast through unknown to the internal
|
|
99
|
+
// AgentCronJob type (which uses Date objects — deserialization is callers'
|
|
100
|
+
// concern, matching prior behaviour).
|
|
101
|
+
return data as unknown as AgentCronJob[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async reconcileSystemCrons(agentId: string): Promise<void> {
|
|
105
|
+
// The reconcile endpoint only defines a 200 response in the spec, so
|
|
106
|
+
// openapi-fetch types `error` as never. Use response.ok instead.
|
|
107
|
+
const { response } = await this.adminClient.POST(
|
|
108
|
+
"/agents/{id}/crons/reconcile",
|
|
109
|
+
{ params: { path: { id: agentId } } },
|
|
110
|
+
);
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new ShipwrightClientError(
|
|
113
|
+
response.status,
|
|
114
|
+
`POST /agents/${agentId}/crons/reconcile failed: ${response.status}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/runtime/slack-account-status.unit.test.ts
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for SSP-8.3's account-status Slack gate: the pure
|
|
5
|
+
* isAccountPaused() predicate, plus handler-level gating for all three
|
|
6
|
+
* inbound Slack event handlers (message, app_mention, reaction_added).
|
|
7
|
+
* Mirrors slack-trial-expiry.unit.test.ts's harness.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
11
|
+
import {
|
|
12
|
+
type AgentAccountStatusRef,
|
|
13
|
+
createAgentAccountStatusRef,
|
|
14
|
+
} from "./agent-account-status-ref.ts";
|
|
15
|
+
import {
|
|
16
|
+
createSlackApp as _createSlackApp,
|
|
17
|
+
ACCOUNT_PAUSED_NOTICE,
|
|
18
|
+
isAccountPaused,
|
|
19
|
+
} from "./slack.ts";
|
|
20
|
+
|
|
21
|
+
function refWith(status: string | null): AgentAccountStatusRef {
|
|
22
|
+
const ref = createAgentAccountStatusRef();
|
|
23
|
+
ref.set(status);
|
|
24
|
+
return ref;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("isAccountPaused", () => {
|
|
28
|
+
test("fails open when the ref has never synced", () => {
|
|
29
|
+
expect(isAccountPaused(createAgentAccountStatusRef())).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("not paused for an agent with no account (null)", () => {
|
|
33
|
+
expect(isAccountPaused(refWith(null))).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("not paused for an active account", () => {
|
|
37
|
+
expect(isAccountPaused(refWith("active"))).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("paused for suspended and trial_expired", () => {
|
|
41
|
+
expect(isAccountPaused(refWith("suspended"))).toBe(true);
|
|
42
|
+
expect(isAccountPaused(refWith("trial_expired"))).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("self-heals once the ref syncs back to active", () => {
|
|
46
|
+
const ref = refWith("suspended");
|
|
47
|
+
expect(isAccountPaused(ref)).toBe(true);
|
|
48
|
+
ref.set("active");
|
|
49
|
+
expect(isAccountPaused(ref)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
type HandlerFn = (...args: unknown[]) => Promise<void>;
|
|
54
|
+
|
|
55
|
+
let capturedMessageHandler: HandlerFn | null = null;
|
|
56
|
+
let capturedMentionHandler: HandlerFn | null = null;
|
|
57
|
+
let capturedReactionAddedHandler: HandlerFn | null = null;
|
|
58
|
+
|
|
59
|
+
class MockApp {
|
|
60
|
+
constructor(_args: Record<string, unknown>) {
|
|
61
|
+
capturedMessageHandler = null;
|
|
62
|
+
capturedMentionHandler = null;
|
|
63
|
+
capturedReactionAddedHandler = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
message(handler: HandlerFn) {
|
|
67
|
+
capturedMessageHandler = handler;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
event(type: string, handler: HandlerFn) {
|
|
71
|
+
if (type === "app_mention") capturedMentionHandler = handler;
|
|
72
|
+
if (type === "reaction_added") capturedReactionAddedHandler = handler;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const mockGatingSlackConfig = {
|
|
77
|
+
botToken: "xoxb-test-token",
|
|
78
|
+
appToken: "xapp-test-token",
|
|
79
|
+
signingSecret: "test-secret",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function makeMockClient() {
|
|
83
|
+
return {
|
|
84
|
+
chat: {
|
|
85
|
+
postMessage: mock(async (_args: unknown) => ({ ts: "resp.ts.1" })),
|
|
86
|
+
startStream: mock(async (_args: unknown) => ({ ts: "stream.ts.1" })),
|
|
87
|
+
appendStream: mock(async (_args: unknown) => {}),
|
|
88
|
+
stopStream: mock(async (_args: unknown) => {}),
|
|
89
|
+
},
|
|
90
|
+
users: {
|
|
91
|
+
info: mock(async (_args: unknown) => ({
|
|
92
|
+
user: {
|
|
93
|
+
profile: { display_name: "Test User", email: "member@example.com" },
|
|
94
|
+
name: "testuser",
|
|
95
|
+
},
|
|
96
|
+
})),
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function makeSay() {
|
|
102
|
+
return mock(async (_args: unknown) => ({ ts: "reply.ts.1" }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function setupGatingApp(overrides: {
|
|
106
|
+
accountStatusRef: AgentAccountStatusRef;
|
|
107
|
+
runner?: (
|
|
108
|
+
message: string,
|
|
109
|
+
sessionKey?: string,
|
|
110
|
+
) => Promise<{
|
|
111
|
+
result: string;
|
|
112
|
+
sessionId?: string;
|
|
113
|
+
streamIncomplete?: boolean;
|
|
114
|
+
}>;
|
|
115
|
+
getSessionFn?: (key: string) => Promise<string | undefined>;
|
|
116
|
+
}) {
|
|
117
|
+
const runner =
|
|
118
|
+
overrides.runner ??
|
|
119
|
+
mock(async (_msg: string, _key?: string) => ({
|
|
120
|
+
result: "Claude response",
|
|
121
|
+
sessionId: "sess-1",
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
_createSlackApp(
|
|
125
|
+
runner,
|
|
126
|
+
(text: string) => text,
|
|
127
|
+
(channel: string, ts: string) => `${channel}:${ts}`,
|
|
128
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock factory for tests
|
|
129
|
+
(cfg) => new MockApp(cfg as Record<string, unknown>) as any,
|
|
130
|
+
mockGatingSlackConfig,
|
|
131
|
+
undefined, // sentryClient — default noop
|
|
132
|
+
async () => null, // fileDownloaderFn
|
|
133
|
+
{}, // voiceConfig
|
|
134
|
+
async () => null, // transcribeAudioFn
|
|
135
|
+
async () => null, // synthesizeSpeechFn
|
|
136
|
+
async (userId: string) => userId, // resolveUserFn (display name) — identity
|
|
137
|
+
"UBOT123", // botUserId
|
|
138
|
+
async () => ({ messages: [] }), // conversationsRepliesFn
|
|
139
|
+
overrides.getSessionFn ?? (async () => undefined), // getSessionFn
|
|
140
|
+
undefined, // blocksConverter — default
|
|
141
|
+
undefined, // chatTokenReporter — default noop
|
|
142
|
+
async () => undefined, // resolveUserEmailFn — default (irrelevant here)
|
|
143
|
+
undefined, // membershipRef — default (irrelevant here)
|
|
144
|
+
undefined, // trialExpiryRef — default (unsynced, fails open)
|
|
145
|
+
overrides.accountStatusRef,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return { runner };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
describe("account-status gating — message handler", () => {
|
|
152
|
+
async function invokeDM(accountStatusRef: AgentAccountStatusRef) {
|
|
153
|
+
const { runner } = setupGatingApp({ accountStatusRef });
|
|
154
|
+
const client = makeMockClient();
|
|
155
|
+
const say = makeSay();
|
|
156
|
+
const message = {
|
|
157
|
+
channel: "D123",
|
|
158
|
+
ts: "111.222",
|
|
159
|
+
text: "Hello bot",
|
|
160
|
+
channel_type: "im",
|
|
161
|
+
user: "U-SENDER",
|
|
162
|
+
};
|
|
163
|
+
await capturedMessageHandler?.({ message, say, client });
|
|
164
|
+
return { runner, say };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const status of [null, "active"]) {
|
|
168
|
+
test(`${status ?? "no account"}: processed as today`, async () => {
|
|
169
|
+
const { runner, say } = await invokeDM(refWith(status));
|
|
170
|
+
expect(runner).toHaveBeenCalledTimes(1);
|
|
171
|
+
expect(say).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const status of ["suspended", "trial_expired"]) {
|
|
176
|
+
test(`${status}: runner not called, one paused notice replied`, async () => {
|
|
177
|
+
const { runner, say } = await invokeDM(refWith(status));
|
|
178
|
+
expect(runner).not.toHaveBeenCalled();
|
|
179
|
+
expect(say).toHaveBeenCalledTimes(1);
|
|
180
|
+
const call = say.mock.calls[0]?.[0] as { text: string };
|
|
181
|
+
expect(call.text).toBe(ACCOUNT_PAUSED_NOTICE);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
test("reactivation: messages are handled again after the next sync", async () => {
|
|
186
|
+
const ref = refWith("suspended");
|
|
187
|
+
expect((await invokeDM(ref)).runner).not.toHaveBeenCalled();
|
|
188
|
+
ref.set("active");
|
|
189
|
+
expect((await invokeDM(ref)).runner).toHaveBeenCalledTimes(1);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("account-status gating — app_mention handler", () => {
|
|
194
|
+
async function invokeMention(accountStatusRef: AgentAccountStatusRef) {
|
|
195
|
+
const { runner } = setupGatingApp({ accountStatusRef });
|
|
196
|
+
const client = makeMockClient();
|
|
197
|
+
const say = makeSay();
|
|
198
|
+
const event = {
|
|
199
|
+
text: "<@UBOT> do something",
|
|
200
|
+
channel: "C999",
|
|
201
|
+
ts: "222.333",
|
|
202
|
+
user: "U-SENDER",
|
|
203
|
+
};
|
|
204
|
+
await capturedMentionHandler?.({ event, say, client });
|
|
205
|
+
return { runner, say };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
test("active: processed as today", async () => {
|
|
209
|
+
const { runner, say } = await invokeMention(refWith("active"));
|
|
210
|
+
expect(runner).toHaveBeenCalledTimes(1);
|
|
211
|
+
expect(say).not.toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("suspended: runner not called, one paused notice replied", async () => {
|
|
215
|
+
const { runner, say } = await invokeMention(refWith("suspended"));
|
|
216
|
+
expect(runner).not.toHaveBeenCalled();
|
|
217
|
+
expect(say).toHaveBeenCalledTimes(1);
|
|
218
|
+
const call = say.mock.calls[0]?.[0] as { text: string };
|
|
219
|
+
expect(call.text).toBe(ACCOUNT_PAUSED_NOTICE);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("account-status gating — reaction_added handler", () => {
|
|
224
|
+
async function invokeReaction(accountStatusRef: AgentAccountStatusRef) {
|
|
225
|
+
const { runner } = setupGatingApp({ accountStatusRef });
|
|
226
|
+
const client = makeMockClient();
|
|
227
|
+
const event = {
|
|
228
|
+
reaction: "thumbsup",
|
|
229
|
+
item: { type: "message", channel: "D1", ts: "100.1" },
|
|
230
|
+
item_user: "UBOT123",
|
|
231
|
+
user: "U-SENDER",
|
|
232
|
+
};
|
|
233
|
+
await capturedReactionAddedHandler?.({ event, client });
|
|
234
|
+
return { runner, client };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
test("active: processed as today", async () => {
|
|
238
|
+
const { runner, client } = await invokeReaction(refWith("active"));
|
|
239
|
+
expect(runner).toHaveBeenCalledTimes(1);
|
|
240
|
+
expect(client.chat.postMessage).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("trial_expired: runner not called, one paused notice posted", async () => {
|
|
244
|
+
const { runner, client } = await invokeReaction(refWith("trial_expired"));
|
|
245
|
+
expect(runner).not.toHaveBeenCalled();
|
|
246
|
+
expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
|
|
247
|
+
const call = (client.chat.postMessage as ReturnType<typeof mock>).mock
|
|
248
|
+
.calls[0]?.[0] as { text: string };
|
|
249
|
+
expect(call.text).toBe(ACCOUNT_PAUSED_NOTICE);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/runtime/slack-bolt-client.integration.test.ts
|
|
3
|
+
*
|
|
4
|
+
* Regression coverage for the streaming API surface SlackProgress
|
|
5
|
+
* (slack-progress.ts) now depends on unconditionally. With the
|
|
6
|
+
* SHIPWRIGHT_SLACK_THINKING_STEPS_ENABLED flag removed (TSD-1.1),
|
|
7
|
+
* streaming is the *only* mode — every agent, on every message, hits
|
|
8
|
+
* `chat.startStream`, `chat.appendStream`, and `chat.stopStream` on the
|
|
9
|
+
* client Bolt hands its event handlers (`app.client`).
|
|
10
|
+
*
|
|
11
|
+
* That client is built internally by @slack/bolt from *its own*
|
|
12
|
+
* dependency's `@slack/web-api` resolution — not the top-level
|
|
13
|
+
* `@slack/web-api` dependency declared in agent/package.json used elsewhere
|
|
14
|
+
* (e.g. the standalone `new WebClient(...)` in index.ts). This is the same
|
|
15
|
+
* class of risk the original setStatus-focused test (#2997/#3003) guarded
|
|
16
|
+
* against, now pointed at the streaming namespace: if Bolt's nested
|
|
17
|
+
* `@slack/web-api` copy ever diverges from (or predates) the top-level one
|
|
18
|
+
* and lacks these streaming methods, `app.client.chat.startStream` is
|
|
19
|
+
* `undefined` at runtime and every progress call silently fails (caught and
|
|
20
|
+
* warn-logged by SlackProgress, never crashing — see slack-progress.ts).
|
|
21
|
+
* The old setStatus/agents.sessions assertion is gone because that code
|
|
22
|
+
* path was removed with the flag.
|
|
23
|
+
*
|
|
24
|
+
* Neither existing suite catches this:
|
|
25
|
+
* - slack-progress.unit.test.ts mocks `client.chat.startStream` etc.
|
|
26
|
+
* directly, so it passes regardless of which @slack/web-api Bolt actually
|
|
27
|
+
* resolves.
|
|
28
|
+
* - slack.integration.test.ts injects a MockApp via createSlackApp's
|
|
29
|
+
* appFactory param — it never constructs a real @slack/bolt App, so it
|
|
30
|
+
* can't observe Bolt's real internal client shape.
|
|
31
|
+
*
|
|
32
|
+
* This test constructs a REAL @slack/bolt App (construction only — no
|
|
33
|
+
* .start(), no live socket-mode connection) with dummy credentials and
|
|
34
|
+
* asserts the resolved dependency tree actually has the streaming APIs on
|
|
35
|
+
* the client Bolt builds. This is the only test in the suite that would
|
|
36
|
+
* catch a future re-pin or transitive bump that un-dedupes the nested
|
|
37
|
+
* @slack/web-api copy from the top-level one.
|
|
38
|
+
*
|
|
39
|
+
* CPS-1.1 follow-up: Bolt 5.x's `App` constructor — independent of
|
|
40
|
+
* `socketMode`/receiver config, confirmed by reading App.js's init()
|
|
41
|
+
* (node_modules/@slack/bolt/dist/App.js) — calls `this.client.auth.test({
|
|
42
|
+
* token })` to verify the token, gated on the constructor's
|
|
43
|
+
* `tokenVerificationEnabled` option (defaults to `true`). With this suite's
|
|
44
|
+
* dummy token that call reached the real Slack API and rejected with
|
|
45
|
+
* WebAPIPlatformError("invalid_auth") shortly *after* construction
|
|
46
|
+
* returned — this test's assertion had already run and the (synchronous)
|
|
47
|
+
* test had already finished, so bun:test had no handle on that promise. It
|
|
48
|
+
* surfaced process-wide as an "Unhandled error between tests" landing in
|
|
49
|
+
* whichever file happened to be running at that moment — the root cause of
|
|
50
|
+
* the "different unrelated test fails every CI run" pattern observed on PR
|
|
51
|
+
* #3006 and #3008. `tokenVerificationEnabled: false` below disables that
|
|
52
|
+
* auth.test() call entirely, matching what this test's own docstring above
|
|
53
|
+
* already promises ("construction only ... no live connection") — the client
|
|
54
|
+
* shape assertion doesn't depend on verifying the dummy token is valid.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import { describe, expect, test } from "bun:test";
|
|
58
|
+
import { App } from "@slack/bolt";
|
|
59
|
+
|
|
60
|
+
describe("real @slack/bolt App — client dependency resolution", () => {
|
|
61
|
+
test("app.client.chat has the streaming APIs SlackProgress depends on", () => {
|
|
62
|
+
const app = new App({
|
|
63
|
+
token: "xoxb-dummy-token",
|
|
64
|
+
appToken: "xapp-dummy-token",
|
|
65
|
+
socketMode: true,
|
|
66
|
+
signingSecret: "dummy-signing-secret",
|
|
67
|
+
tokenVerificationEnabled: false,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// The three streaming methods SlackProgress now calls unconditionally in
|
|
71
|
+
// every mode (flag removed, TSD-1.1) — see slack-progress.ts.
|
|
72
|
+
expect(typeof app.client.chat.startStream).toBe("function");
|
|
73
|
+
expect(typeof app.client.chat.appendStream).toBe("function");
|
|
74
|
+
expect(typeof app.client.chat.stopStream).toBe("function");
|
|
75
|
+
});
|
|
76
|
+
});
|