@app-vitals/agent-runtime 0.0.1 → 0.1.1
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 +50 -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,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/runtime/setup-github-auth.ts
|
|
3
|
+
*
|
|
4
|
+
* Wires GitHub authentication on agent startup.
|
|
5
|
+
*
|
|
6
|
+
* - GitHub App path (GH_APP_ID + GH_APP_PRIVATE_KEY): engages only if the
|
|
7
|
+
* GH_APP_INSTALLATION_ID pin is set or installation discovery finds at
|
|
8
|
+
* least one usable in-scope installation. Mints the default installation's
|
|
9
|
+
* token into the token file (plus a per-owner file for every
|
|
10
|
+
* usable installation, even a single one), configures the git credential helper + author identity, and
|
|
11
|
+
* starts exactly one background refresh.
|
|
12
|
+
* - PAT path: runs `gh auth setup-git` (legacy flow preserved) whenever App
|
|
13
|
+
* auth did not engage.
|
|
14
|
+
* - Neither: skips; the caller retries on its next tick.
|
|
15
|
+
*
|
|
16
|
+
* Resolves true iff App auth activated. Not safely re-invokable after it
|
|
17
|
+
* resolves true — each activation starts its own refresh interval — so
|
|
18
|
+
* callers gate repeat calls on that result (github-auth-startup.ts).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { TokenFilesInput } from "./gh-token-files.ts";
|
|
22
|
+
import type { BotIdentity } from "./github-app-auth.ts";
|
|
23
|
+
import type { InstallationsSnapshot } from "./github-installations.ts";
|
|
24
|
+
|
|
25
|
+
interface TokenManagerLike {
|
|
26
|
+
getToken(): Promise<string>;
|
|
27
|
+
startBackgroundRefresh(onRefresh: (token: string) => Promise<void>): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GitHubAuthDeps {
|
|
31
|
+
env: Record<string, string | undefined>;
|
|
32
|
+
createTokenManager: () => TokenManagerLike;
|
|
33
|
+
getBotIdentity: () => Promise<BotIdentity>;
|
|
34
|
+
spawnSync: (
|
|
35
|
+
cmd: string,
|
|
36
|
+
args: string[],
|
|
37
|
+
opts: {
|
|
38
|
+
stdio: "inherit" | "pipe" | "ignore";
|
|
39
|
+
env: Record<string, string | undefined>;
|
|
40
|
+
},
|
|
41
|
+
) => { status: number | null };
|
|
42
|
+
writeToken: (token: string) => void;
|
|
43
|
+
tokenPath: string;
|
|
44
|
+
credentialHelperPath: string;
|
|
45
|
+
logger?: { error: (...args: unknown[]) => void };
|
|
46
|
+
/**
|
|
47
|
+
* Installation discovery. When absent, App auth requires the
|
|
48
|
+
* GH_APP_INSTALLATION_ID pin and uses the single-installation token
|
|
49
|
+
* manager (the pre-discovery behavior).
|
|
50
|
+
*/
|
|
51
|
+
installations?: GitHubInstallationsDeps;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The slice of GitHubInstallationsManager setupGitHubAuth drives. */
|
|
55
|
+
interface InstallationsManagerLike {
|
|
56
|
+
reconcile(scopeOwners: string[] | null): Promise<void>;
|
|
57
|
+
getState(): InstallationsSnapshot;
|
|
58
|
+
getToken(installationId: number): Promise<string>;
|
|
59
|
+
start(): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface GitHubInstallationsDeps {
|
|
63
|
+
/**
|
|
64
|
+
* Builds a manager; it must call `onTokensChanged` whenever a token may
|
|
65
|
+
* have changed (mint, or installation set/health change).
|
|
66
|
+
*/
|
|
67
|
+
createManager: (onTokensChanged: () => void) => InstallationsManagerLike;
|
|
68
|
+
/** Scoped repo owners, or null while scope is unsynced (pinned-only). */
|
|
69
|
+
getScopeOwners: () => string[] | null;
|
|
70
|
+
writeTokenFiles: (input: TokenFilesInput) => void;
|
|
71
|
+
/** Called once with the manager App auth activated with. */
|
|
72
|
+
onActivated?: (manager: InstallationsManagerLike) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function runOrWarn(
|
|
76
|
+
spawnSync: GitHubAuthDeps["spawnSync"],
|
|
77
|
+
cmd: string,
|
|
78
|
+
args: string[],
|
|
79
|
+
env: Record<string, string | undefined>,
|
|
80
|
+
logger: { error: (...args: unknown[]) => void } = console,
|
|
81
|
+
): void {
|
|
82
|
+
const { status } = spawnSync(cmd, args, { stdio: "inherit", env });
|
|
83
|
+
if (status !== 0) {
|
|
84
|
+
logger.error(
|
|
85
|
+
`[entrypoint] ${cmd} ${args.join(" ")} exited with status ${status} — git auth may be broken`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function setupGitHubAuth(deps: GitHubAuthDeps): Promise<boolean> {
|
|
91
|
+
const { env, spawnSync, logger } = deps;
|
|
92
|
+
|
|
93
|
+
// Repos on the PVC may be owned by root (uid 0) while the agent runs as uid 1000.
|
|
94
|
+
// Set safe.directory unconditionally so git can operate on them regardless of auth path.
|
|
95
|
+
runOrWarn(
|
|
96
|
+
spawnSync,
|
|
97
|
+
"git",
|
|
98
|
+
["config", "--global", "safe.directory", "*"],
|
|
99
|
+
env,
|
|
100
|
+
logger,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const appId = env.GH_APP_ID;
|
|
104
|
+
const installationId = env.GH_APP_INSTALLATION_ID;
|
|
105
|
+
const privateKey = env.GH_APP_PRIVATE_KEY;
|
|
106
|
+
|
|
107
|
+
if (appId && privateKey) {
|
|
108
|
+
if (deps.installations) {
|
|
109
|
+
const pending = await prepareInstallations(deps, deps.installations);
|
|
110
|
+
if (pending) {
|
|
111
|
+
const identity = await configureAppGit(deps);
|
|
112
|
+
// Started only after git config succeeded, so a failed setup (retried
|
|
113
|
+
// next tick) never leaves a refresh interval behind.
|
|
114
|
+
pending.start();
|
|
115
|
+
logAppConfigured(identity);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (installationId) {
|
|
120
|
+
await setupPinnedTokenManager(deps);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (env.GH_TOKEN) {
|
|
126
|
+
runOrWarn(spawnSync, "gh", ["auth", "setup-git"], env, logger);
|
|
127
|
+
console.log(
|
|
128
|
+
"[entrypoint] GitHub PAT auth configured — git credential helper installed",
|
|
129
|
+
);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(
|
|
134
|
+
"[entrypoint] No usable GitHub credentials — skipping GitHub setup",
|
|
135
|
+
);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Points git at the token file and sets the App bot as commit author. */
|
|
140
|
+
async function configureAppGit(deps: GitHubAuthDeps): Promise<AppIdentity> {
|
|
141
|
+
const { env, spawnSync, tokenPath, credentialHelperPath, logger } = deps;
|
|
142
|
+
env.GH_TOKEN_FILE = tokenPath;
|
|
143
|
+
runOrWarn(
|
|
144
|
+
spawnSync,
|
|
145
|
+
"git",
|
|
146
|
+
[
|
|
147
|
+
"config",
|
|
148
|
+
"--global",
|
|
149
|
+
"credential.https://github.com.helper",
|
|
150
|
+
`!${credentialHelperPath}`,
|
|
151
|
+
],
|
|
152
|
+
env,
|
|
153
|
+
logger,
|
|
154
|
+
);
|
|
155
|
+
// Makes git send the repo path to the helper so it can pick the per-owner
|
|
156
|
+
// token. Host-scoped only; the helper ignores it unless gh-token.d exists.
|
|
157
|
+
runOrWarn(
|
|
158
|
+
spawnSync,
|
|
159
|
+
"git",
|
|
160
|
+
["config", "--global", "credential.https://github.com.useHttpPath", "true"],
|
|
161
|
+
env,
|
|
162
|
+
logger,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const { slug, userId } = await deps.getBotIdentity();
|
|
166
|
+
const botEmail = `${userId}+${slug}[bot]@users.noreply.github.com`;
|
|
167
|
+
runOrWarn(
|
|
168
|
+
spawnSync,
|
|
169
|
+
"git",
|
|
170
|
+
["config", "--global", "user.name", `${slug}[bot]`],
|
|
171
|
+
env,
|
|
172
|
+
logger,
|
|
173
|
+
);
|
|
174
|
+
runOrWarn(
|
|
175
|
+
spawnSync,
|
|
176
|
+
"git",
|
|
177
|
+
["config", "--global", "user.email", botEmail],
|
|
178
|
+
env,
|
|
179
|
+
logger,
|
|
180
|
+
);
|
|
181
|
+
return { slug, userId };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
type AppIdentity = Pick<BotIdentity, "slug" | "userId">;
|
|
185
|
+
|
|
186
|
+
function logAppConfigured({ slug, userId }: AppIdentity): void {
|
|
187
|
+
console.log(
|
|
188
|
+
`[entrypoint] GitHub App auth configured as ${slug}[bot] (user ${userId}), background refresh started`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Single pinned installation via its own token manager (pre-discovery path). */
|
|
193
|
+
async function setupPinnedTokenManager(deps: GitHubAuthDeps): Promise<void> {
|
|
194
|
+
console.log(
|
|
195
|
+
"[entrypoint] GitHub App credentials detected — initializing token manager",
|
|
196
|
+
);
|
|
197
|
+
const manager = deps.createTokenManager();
|
|
198
|
+
deps.writeToken(await manager.getToken());
|
|
199
|
+
const identity = await configureAppGit(deps);
|
|
200
|
+
manager.startBackgroundRefresh(async (refreshedToken) => {
|
|
201
|
+
deps.writeToken(refreshedToken);
|
|
202
|
+
});
|
|
203
|
+
logAppConfigured(identity);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Discovers installations and, if the pin or at least one in-scope
|
|
208
|
+
* installation is usable, writes the token files and returns a `start`
|
|
209
|
+
* handle that begins the manager's single refresh timer and publishes it via
|
|
210
|
+
* `onActivated`. Returns null (nothing written or started) otherwise.
|
|
211
|
+
*/
|
|
212
|
+
async function prepareInstallations(
|
|
213
|
+
deps: GitHubAuthDeps,
|
|
214
|
+
inst: GitHubInstallationsDeps,
|
|
215
|
+
): Promise<{ start: () => void } | null> {
|
|
216
|
+
let active = false;
|
|
217
|
+
let writing: Promise<unknown> = Promise.resolve();
|
|
218
|
+
const manager = inst.createManager(() => {
|
|
219
|
+
if (!active) return;
|
|
220
|
+
writing = writing.then(() => writeInstallationTokens(manager, inst, deps));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await manager.reconcile(inst.getScopeOwners());
|
|
224
|
+
const { installations } = manager.getState();
|
|
225
|
+
if (!installations.some((i) => i.health !== "broken")) return null;
|
|
226
|
+
|
|
227
|
+
console.log(
|
|
228
|
+
`[entrypoint] GitHub App credentials detected — ${installations.length} installation(s) selected`,
|
|
229
|
+
);
|
|
230
|
+
if (!(await writeInstallationTokens(manager, inst, deps, true))) return null;
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
start: () => {
|
|
234
|
+
active = true;
|
|
235
|
+
manager.start();
|
|
236
|
+
inst.onActivated?.(manager);
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Writes `gh-token` (default installation) and per-owner tokens for every
|
|
243
|
+
* installation that mints. Returns false if no default token could be minted.
|
|
244
|
+
* Never throws unless `throwOnError` — refresh-time failures keep the last
|
|
245
|
+
* good files.
|
|
246
|
+
*/
|
|
247
|
+
async function writeInstallationTokens(
|
|
248
|
+
manager: InstallationsManagerLike,
|
|
249
|
+
inst: GitHubInstallationsDeps,
|
|
250
|
+
deps: GitHubAuthDeps,
|
|
251
|
+
throwOnError = false,
|
|
252
|
+
): Promise<boolean> {
|
|
253
|
+
try {
|
|
254
|
+
const { installations, defaultId } = manager.getState();
|
|
255
|
+
const ownerTokens: Record<string, string> = {};
|
|
256
|
+
const tokenById = new Map<number, string>();
|
|
257
|
+
const failedOwners: string[] = [];
|
|
258
|
+
for (const i of installations) {
|
|
259
|
+
if (i.health === "broken") continue;
|
|
260
|
+
try {
|
|
261
|
+
const token = await manager.getToken(i.id);
|
|
262
|
+
tokenById.set(i.id, token);
|
|
263
|
+
if (i.owner) ownerTokens[i.owner] = token;
|
|
264
|
+
} catch {
|
|
265
|
+
// a failed mint never blocks the others; refresh keeps its last good file
|
|
266
|
+
if (i.owner) failedOwners.push(i.owner);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const defaultMinted =
|
|
270
|
+
defaultId !== null ? tokenById.get(defaultId) : undefined;
|
|
271
|
+
// Boot falls back to any minted token; refresh never substitutes another
|
|
272
|
+
// installation's token for the default.
|
|
273
|
+
const defaultToken = throwOnError
|
|
274
|
+
? (defaultMinted ?? tokenById.values().next().value)
|
|
275
|
+
: defaultMinted;
|
|
276
|
+
if (throwOnError) {
|
|
277
|
+
if (defaultToken === undefined) return false;
|
|
278
|
+
inst.writeTokenFiles({ defaultToken, ownerTokens });
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (defaultToken === undefined && tokenById.size === 0) return false;
|
|
282
|
+
for (const owner of failedOwners) {
|
|
283
|
+
console.log(
|
|
284
|
+
`[github-auth] mint failed for ${owner} — keeping last good token file`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
inst.writeTokenFiles({ defaultToken, ownerTokens, failedOwners });
|
|
288
|
+
return true;
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (throwOnError) throw err;
|
|
291
|
+
(deps.logger ?? console).error(
|
|
292
|
+
"[github-auth] token file write failed:",
|
|
293
|
+
err instanceof Error ? err.message : String(err),
|
|
294
|
+
);
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AGR-1.9 — the platform seeds workspaces without any product specialty.
|
|
3
|
+
*
|
|
4
|
+
* Real temp dirs, no mocks. The fixture specialty under fixtures/specialties/
|
|
5
|
+
* stands in for a product specialty; nothing here reads a coding template.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { ensureAgentHome, resolveTemplatesDir } from "./setup.ts";
|
|
18
|
+
|
|
19
|
+
// Beside the runtime source in the extracted layout, one level up in the monorepo.
|
|
20
|
+
const FIXTURE_SPECIALTIES = [
|
|
21
|
+
join(import.meta.dir, "fixtures", "specialties"),
|
|
22
|
+
join(import.meta.dir, "..", "fixtures", "specialties"),
|
|
23
|
+
].find((dir) => existsSync(dir)) as string;
|
|
24
|
+
const FIXTURE_TEMPLATES = join(FIXTURE_SPECIALTIES, "fixture", "templates");
|
|
25
|
+
const ENV = {
|
|
26
|
+
SHIPWRIGHT_SPECIALTIES_DIR: FIXTURE_SPECIALTIES,
|
|
27
|
+
SHIPWRIGHT_DEFAULT_AGENT_TYPE: "fixture",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let root: string;
|
|
31
|
+
let home: string;
|
|
32
|
+
let ws: string;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
root = `/tmp/test-agr19-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
36
|
+
home = join(root, "home");
|
|
37
|
+
ws = join(home, "workspace");
|
|
38
|
+
mkdirSync(home, { recursive: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
rmSync(root, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const read = (...p: string[]) => readFileSync(join(...p), "utf8");
|
|
46
|
+
|
|
47
|
+
describe("fixture specialty templates", () => {
|
|
48
|
+
it("resolves the fixture templates from the manifest without a warning", () => {
|
|
49
|
+
const warnings: string[] = [];
|
|
50
|
+
const dir = resolveTemplatesDir(
|
|
51
|
+
"fixture",
|
|
52
|
+
root,
|
|
53
|
+
(m) => warnings.push(m),
|
|
54
|
+
ENV,
|
|
55
|
+
);
|
|
56
|
+
expect(dir).toBe(FIXTURE_TEMPLATES);
|
|
57
|
+
expect(warnings).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("falls back to the configured default type when none is requested", () => {
|
|
61
|
+
const dir = resolveTemplatesDir(undefined, root, () => {}, ENV);
|
|
62
|
+
expect(dir).toBe(FIXTURE_TEMPLATES);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("seeds a new workspace from the fixture templates", () => {
|
|
66
|
+
const dir = resolveTemplatesDir("fixture", root, () => {}, ENV);
|
|
67
|
+
ensureAgentHome(home, "fixture", dir);
|
|
68
|
+
|
|
69
|
+
for (const name of ["CLAUDE", "SOUL", "IDENTITY"]) {
|
|
70
|
+
expect(read(ws, `${name}.md`)).toBe(
|
|
71
|
+
read(FIXTURE_TEMPLATES, `${name}.md.template`),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
expect(read(ws, "CLAUDE.md")).toContain("@SOUL.md");
|
|
75
|
+
expect(existsSync(join(ws, "RUNTIME.md"))).toBe(true);
|
|
76
|
+
expect(existsSync(join(ws, "state", "reviews"))).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("never rewrites CLAUDE/SOUL/IDENTITY on a later boot but refreshes RUNTIME.md", () => {
|
|
80
|
+
const dir = resolveTemplatesDir("fixture", root, () => {}, ENV);
|
|
81
|
+
ensureAgentHome(home, "fixture", dir);
|
|
82
|
+
for (const n of ["CLAUDE", "SOUL", "IDENTITY"])
|
|
83
|
+
writeFileSync(join(ws, `${n}.md`), "mine\n");
|
|
84
|
+
writeFileSync(join(ws, "RUNTIME.md"), "stale\n");
|
|
85
|
+
|
|
86
|
+
ensureAgentHome(home, "fixture", dir);
|
|
87
|
+
|
|
88
|
+
for (const n of ["CLAUDE", "SOUL", "IDENTITY"])
|
|
89
|
+
expect(read(ws, `${n}.md`)).toBe("mine\n");
|
|
90
|
+
expect(read(ws, "RUNTIME.md")).not.toBe("stale\n");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("no default specialty configured", () => {
|
|
95
|
+
it("names the env vars when the default specialty's templates cannot be found", () => {
|
|
96
|
+
const emptyDir = join(root, "empty-templates");
|
|
97
|
+
mkdirSync(emptyDir, { recursive: true });
|
|
98
|
+
// An unresolvable default with no built-in templates to fall back to.
|
|
99
|
+
let message = "";
|
|
100
|
+
try {
|
|
101
|
+
ensureAgentHome(home, "absent", emptyDir);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
message = (err as Error).message;
|
|
104
|
+
}
|
|
105
|
+
expect(message).toContain("Missing workspace template");
|
|
106
|
+
expect(message).toContain("SHIPWRIGHT_SPECIALTIES_DIR");
|
|
107
|
+
expect(message).toContain("SHIPWRIGHT_DEFAULT_AGENT_TYPE");
|
|
108
|
+
});
|
|
109
|
+
});
|