stagent 0.9.5 → 0.10.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 +5 -42
- package/dist/cli.js +42 -18
- package/docs/.coverage-gaps.json +13 -55
- package/docs/.last-generated +1 -1
- package/docs/features/provider-runtimes.md +4 -0
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/tables.md +9 -2
- package/docs/features/workflows.md +10 -4
- package/docs/journeys/developer.md +15 -1
- package/docs/journeys/personal-use.md +21 -4
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
- package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
- package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
- package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
- package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
- package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
- package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
- package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
- package/package.json +3 -2
- package/src/__tests__/instrumentation-smoke.test.ts +15 -0
- package/src/app/analytics/page.tsx +1 -21
- package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
- package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
- package/src/app/api/instance/config/route.ts +41 -0
- package/src/app/api/instance/init/route.ts +34 -0
- package/src/app/api/instance/upgrade/check/route.ts +26 -0
- package/src/app/api/instance/upgrade/route.ts +96 -0
- package/src/app/api/instance/upgrade/status/route.ts +35 -0
- package/src/app/api/memory/route.ts +0 -11
- package/src/app/api/notifications/route.ts +4 -2
- package/src/app/api/projects/[id]/route.ts +5 -155
- package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
- package/src/app/api/schedules/[id]/execute/route.ts +111 -0
- package/src/app/api/schedules/[id]/route.ts +9 -1
- package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
- package/src/app/api/schedules/route.ts +3 -12
- package/src/app/api/settings/openai/login/route.ts +22 -0
- package/src/app/api/settings/openai/logout/route.ts +7 -0
- package/src/app/api/settings/openai/route.ts +21 -1
- package/src/app/api/settings/providers/route.ts +35 -8
- package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
- package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
- package/src/app/api/tables/[id]/enrich/route.ts +147 -0
- package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
- package/src/app/api/tasks/[id]/execute/route.ts +0 -21
- package/src/app/api/workflows/[id]/resume/route.ts +59 -0
- package/src/app/api/workflows/[id]/status/route.ts +22 -8
- package/src/app/api/workspace/context/route.ts +2 -0
- package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
- package/src/app/chat/page.tsx +11 -0
- package/src/app/inbox/page.tsx +12 -5
- package/src/app/layout.tsx +42 -21
- package/src/app/page.tsx +0 -2
- package/src/app/settings/page.tsx +6 -9
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
- package/src/components/chat/chat-command-popover.tsx +2 -2
- package/src/components/chat/chat-input.tsx +2 -3
- package/src/components/chat/chat-session-provider.tsx +720 -0
- package/src/components/chat/chat-shell.tsx +92 -401
- package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
- package/src/components/instance/instance-section.tsx +382 -0
- package/src/components/instance/upgrade-badge.tsx +219 -0
- package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
- package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
- package/src/components/notifications/batch-proposal-review.tsx +20 -5
- package/src/components/notifications/inbox-list.tsx +11 -2
- package/src/components/notifications/notification-item.tsx +56 -2
- package/src/components/notifications/pending-approval-host.tsx +56 -37
- package/src/components/schedules/schedule-create-sheet.tsx +19 -1
- package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
- package/src/components/schedules/schedule-form.tsx +31 -0
- package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
- package/src/components/settings/auth-method-selector.tsx +19 -4
- package/src/components/settings/auth-status-badge.tsx +28 -3
- package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
- package/src/components/settings/openai-runtime-section.tsx +7 -1
- package/src/components/settings/providers-runtimes-section.tsx +138 -19
- package/src/components/shared/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +4 -5
- package/src/components/shared/theme-toggle.tsx +5 -24
- package/src/components/shared/workspace-indicator.tsx +61 -2
- package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
- package/src/components/tables/table-create-sheet.tsx +4 -0
- package/src/components/tables/table-enrichment-runs.tsx +103 -0
- package/src/components/tables/table-enrichment-sheet.tsx +538 -0
- package/src/components/tables/table-spreadsheet.tsx +29 -5
- package/src/components/tables/table-toolbar.tsx +10 -1
- package/src/components/tasks/kanban-board.tsx +1 -0
- package/src/components/tasks/kanban-column.tsx +53 -14
- package/src/components/tasks/task-bento-grid.tsx +19 -0
- package/src/components/tasks/task-card.tsx +26 -3
- package/src/components/tasks/task-chip-bar.tsx +24 -0
- package/src/components/tasks/task-result-renderer.tsx +1 -1
- package/src/components/workflows/delay-step-body.tsx +109 -0
- package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
- package/src/components/workflows/loop-status-view.tsx +1 -1
- package/src/components/workflows/shared/step-result.tsx +78 -0
- package/src/components/workflows/shared/workflow-header.tsx +141 -0
- package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
- package/src/components/workflows/swarm-dashboard.tsx +2 -15
- package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
- package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
- package/src/components/workflows/workflow-form-view.tsx +133 -16
- package/src/components/workflows/workflow-status-view.tsx +30 -740
- package/src/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
- package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
- package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
- package/src/lib/agents/claude-agent.ts +155 -18
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +29 -60
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
- package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
- package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
- package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/codex-engine.ts +16 -17
- package/src/lib/chat/context-builder.ts +5 -3
- package/src/lib/chat/engine.ts +50 -3
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/stagent-tools.ts +1 -0
- package/src/lib/chat/stream-telemetry.ts +132 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +26 -1
- package/src/lib/chat/tool-catalog.ts +2 -1
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +39 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- package/src/lib/chat/tools/project-tools.ts +33 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -11
- package/src/lib/chat/tools/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +84 -20
- package/src/lib/chat/tools/workflow-tools.ts +234 -32
- package/src/lib/constants/settings.ts +8 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +20 -15
- package/src/lib/data/delete-project.ts +171 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
- package/src/lib/db/bootstrap.ts +45 -16
- package/src/lib/db/index.ts +5 -0
- package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
- package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
- package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
- package/src/lib/db/migrations/0026_drop_license.sql +3 -0
- package/src/lib/db/migrations/meta/_journal.json +21 -0
- package/src/lib/db/schema.ts +68 -23
- package/src/lib/environment/workspace-context.ts +13 -1
- package/src/lib/import/dedup.ts +4 -54
- package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
- package/src/lib/instance/__tests__/detect.test.ts +115 -0
- package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
- package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
- package/src/lib/instance/__tests__/settings.test.ts +83 -0
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +78 -0
- package/src/lib/instance/git-ops.ts +95 -0
- package/src/lib/instance/settings.ts +61 -0
- package/src/lib/instance/types.ts +77 -0
- package/src/lib/instance/upgrade-poller.ts +153 -0
- package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
- package/src/lib/notifications/visibility.ts +33 -0
- package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
- package/src/lib/schedules/__tests__/config.test.ts +62 -0
- package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
- package/src/lib/schedules/__tests__/integration.test.ts +82 -0
- package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
- package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
- package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
- package/src/lib/schedules/collision-check.ts +105 -0
- package/src/lib/schedules/config.ts +53 -0
- package/src/lib/schedules/scheduler.ts +232 -13
- package/src/lib/schedules/slot-claim.ts +105 -0
- package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
- package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
- package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
- package/src/lib/settings/openai-auth.ts +105 -10
- package/src/lib/settings/openai-login-manager.ts +260 -0
- package/src/lib/settings/runtime-setup.ts +14 -4
- package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
- package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
- package/src/lib/tables/enrichment-planner.ts +454 -0
- package/src/lib/tables/enrichment.ts +328 -0
- package/src/lib/tables/query-builder.ts +5 -2
- package/src/lib/tables/trigger-evaluator.ts +3 -2
- package/src/lib/theme.ts +71 -0
- package/src/lib/usage/ledger.ts +2 -18
- package/src/lib/util/__tests__/similarity.test.ts +106 -0
- package/src/lib/util/similarity.ts +77 -0
- package/src/lib/utils/format-timestamp.ts +24 -0
- package/src/lib/utils/stagent-paths.ts +12 -0
- package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
- package/src/lib/validators/__tests__/settings.test.ts +10 -0
- package/src/lib/validators/blueprint.ts +70 -9
- package/src/lib/validators/profile.ts +2 -2
- package/src/lib/validators/settings.ts +3 -1
- package/src/lib/workflows/__tests__/delay.test.ts +196 -0
- package/src/lib/workflows/__tests__/engine.test.ts +8 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
- package/src/lib/workflows/blueprints/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/types.ts +10 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +207 -4
- package/src/lib/workflows/loop-executor.ts +349 -24
- package/src/lib/workflows/post-action.ts +91 -0
- package/src/lib/workflows/types.ts +166 -1
- package/src/app/api/license/checkout/route.ts +0 -28
- package/src/app/api/license/portal/route.ts +0 -26
- package/src/app/api/license/route.ts +0 -89
- package/src/app/api/license/usage/route.ts +0 -63
- package/src/app/api/marketplace/browse/route.ts +0 -15
- package/src/app/api/marketplace/import/route.ts +0 -28
- package/src/app/api/marketplace/publish/route.ts +0 -40
- package/src/app/api/onboarding/email/route.ts +0 -53
- package/src/app/api/settings/telemetry/route.ts +0 -14
- package/src/app/api/sync/export/route.ts +0 -54
- package/src/app/api/sync/restore/route.ts +0 -37
- package/src/app/api/sync/sessions/route.ts +0 -24
- package/src/app/auth/callback/route.ts +0 -73
- package/src/app/marketplace/page.tsx +0 -19
- package/src/components/analytics/analytics-gate-card.tsx +0 -101
- package/src/components/marketplace/blueprint-card.tsx +0 -61
- package/src/components/marketplace/marketplace-browser.tsx +0 -131
- package/src/components/onboarding/email-capture-card.tsx +0 -104
- package/src/components/settings/activation-form.tsx +0 -95
- package/src/components/settings/cloud-account-section.tsx +0 -147
- package/src/components/settings/cloud-sync-section.tsx +0 -155
- package/src/components/settings/subscription-section.tsx +0 -410
- package/src/components/settings/telemetry-section.tsx +0 -80
- package/src/components/shared/premium-gate-overlay.tsx +0 -50
- package/src/components/shared/schedule-gate-dialog.tsx +0 -64
- package/src/components/shared/upgrade-banner.tsx +0 -112
- package/src/hooks/use-supabase-auth.ts +0 -79
- package/src/lib/billing/email.ts +0 -54
- package/src/lib/billing/products.ts +0 -80
- package/src/lib/billing/stripe.ts +0 -101
- package/src/lib/cloud/supabase-browser.ts +0 -32
- package/src/lib/cloud/supabase-client.ts +0 -56
- package/src/lib/license/__tests__/features.test.ts +0 -56
- package/src/lib/license/__tests__/key-format.test.ts +0 -88
- package/src/lib/license/__tests__/manager.test.ts +0 -64
- package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
- package/src/lib/license/cloud-validation.ts +0 -60
- package/src/lib/license/features.ts +0 -44
- package/src/lib/license/key-format.ts +0 -101
- package/src/lib/license/limit-check.ts +0 -111
- package/src/lib/license/limit-queries.ts +0 -51
- package/src/lib/license/manager.ts +0 -345
- package/src/lib/license/notifications.ts +0 -59
- package/src/lib/license/tier-limits.ts +0 -71
- package/src/lib/marketplace/marketplace-client.ts +0 -107
- package/src/lib/sync/cloud-sync.ts +0 -235
- package/src/lib/telemetry/conversion-events.ts +0 -71
- package/src/lib/telemetry/queue.ts +0 -122
- package/src/lib/validators/license.ts +0 -33
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir, homedir } from "os";
|
|
5
|
+
|
|
6
|
+
let tempDir: string;
|
|
7
|
+
let gitDir: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tempDir = mkdtempSync(join(tmpdir(), "stagent-detect-"));
|
|
11
|
+
gitDir = join(tempDir, ".git");
|
|
12
|
+
mkdirSync(gitDir, { recursive: true });
|
|
13
|
+
vi.resetModules();
|
|
14
|
+
vi.unstubAllEnvs();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.unstubAllEnvs();
|
|
19
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
async function loadDetect() {
|
|
23
|
+
return await import("../detect");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("isDevMode", () => {
|
|
27
|
+
it("returns true when STAGENT_DEV_MODE=true", async () => {
|
|
28
|
+
vi.stubEnv("STAGENT_DEV_MODE", "true");
|
|
29
|
+
const { isDevMode } = await loadDetect();
|
|
30
|
+
expect(isDevMode(tempDir)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns true when .git/stagent-dev-mode sentinel file exists", async () => {
|
|
34
|
+
writeFileSync(join(gitDir, "stagent-dev-mode"), "");
|
|
35
|
+
const { isDevMode } = await loadDetect();
|
|
36
|
+
expect(isDevMode(tempDir)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns false when neither gate is set", async () => {
|
|
40
|
+
const { isDevMode } = await loadDetect();
|
|
41
|
+
expect(isDevMode(tempDir)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns false when STAGENT_INSTANCE_MODE=true overrides env gate", async () => {
|
|
45
|
+
vi.stubEnv("STAGENT_DEV_MODE", "true");
|
|
46
|
+
vi.stubEnv("STAGENT_INSTANCE_MODE", "true");
|
|
47
|
+
const { isDevMode } = await loadDetect();
|
|
48
|
+
expect(isDevMode(tempDir)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns false when STAGENT_INSTANCE_MODE=true overrides sentinel gate", async () => {
|
|
52
|
+
writeFileSync(join(gitDir, "stagent-dev-mode"), "");
|
|
53
|
+
vi.stubEnv("STAGENT_INSTANCE_MODE", "true");
|
|
54
|
+
const { isDevMode } = await loadDetect();
|
|
55
|
+
expect(isDevMode(tempDir)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("hasGitDir", () => {
|
|
60
|
+
it("returns true when .git directory exists", async () => {
|
|
61
|
+
const { hasGitDir } = await loadDetect();
|
|
62
|
+
expect(hasGitDir(tempDir)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns false when .git is absent", async () => {
|
|
66
|
+
rmSync(gitDir, { recursive: true, force: true });
|
|
67
|
+
const { hasGitDir } = await loadDetect();
|
|
68
|
+
expect(hasGitDir(tempDir)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("isPrivateInstance", () => {
|
|
73
|
+
it("returns false when STAGENT_DATA_DIR is unset", async () => {
|
|
74
|
+
vi.stubEnv("STAGENT_DATA_DIR", "");
|
|
75
|
+
const { isPrivateInstance } = await loadDetect();
|
|
76
|
+
expect(isPrivateInstance()).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns false when STAGENT_DATA_DIR equals default ~/.stagent", async () => {
|
|
80
|
+
vi.stubEnv("STAGENT_DATA_DIR", join(homedir(), ".stagent"));
|
|
81
|
+
const { isPrivateInstance } = await loadDetect();
|
|
82
|
+
expect(isPrivateInstance()).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns true when STAGENT_DATA_DIR is a custom path", async () => {
|
|
86
|
+
vi.stubEnv("STAGENT_DATA_DIR", "/Users/navam/.stagent-wealth");
|
|
87
|
+
const { isPrivateInstance } = await loadDetect();
|
|
88
|
+
expect(isPrivateInstance()).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("returns false when STAGENT_DATA_DIR equals default with trailing slash", async () => {
|
|
92
|
+
vi.stubEnv("STAGENT_DATA_DIR", join(homedir(), ".stagent") + "/");
|
|
93
|
+
const { isPrivateInstance } = await loadDetect();
|
|
94
|
+
expect(isPrivateInstance()).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("detectRebaseInProgress", () => {
|
|
99
|
+
it("returns true when .git/rebase-merge exists", async () => {
|
|
100
|
+
mkdirSync(join(gitDir, "rebase-merge"));
|
|
101
|
+
const { detectRebaseInProgress } = await loadDetect();
|
|
102
|
+
expect(detectRebaseInProgress(tempDir)).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns true when .git/rebase-apply exists", async () => {
|
|
106
|
+
mkdirSync(join(gitDir, "rebase-apply"));
|
|
107
|
+
const { detectRebaseInProgress } = await loadDetect();
|
|
108
|
+
expect(detectRebaseInProgress(tempDir)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns false when no rebase state directories exist", async () => {
|
|
112
|
+
const { detectRebaseInProgress } = await loadDetect();
|
|
113
|
+
expect(detectRebaseInProgress(tempDir)).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
beforeEach(() => {
|
|
4
|
+
vi.resetModules();
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.restoreAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("getMachineFingerprint", () => {
|
|
12
|
+
it("returns a 64-character hex SHA-256 string", async () => {
|
|
13
|
+
const { getMachineFingerprint, _resetFingerprintCache } = await import("../fingerprint");
|
|
14
|
+
_resetFingerprintCache();
|
|
15
|
+
const fp = getMachineFingerprint();
|
|
16
|
+
expect(fp).toMatch(/^[a-f0-9]{64}$/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("is stable across multiple calls in the same process (memoized)", async () => {
|
|
20
|
+
const { getMachineFingerprint, _resetFingerprintCache } = await import("../fingerprint");
|
|
21
|
+
_resetFingerprintCache();
|
|
22
|
+
const fp1 = getMachineFingerprint();
|
|
23
|
+
const fp2 = getMachineFingerprint();
|
|
24
|
+
const fp3 = getMachineFingerprint();
|
|
25
|
+
expect(fp1).toBe(fp2);
|
|
26
|
+
expect(fp2).toBe(fp3);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("does not contain the raw hostname, username, or MAC address", async () => {
|
|
30
|
+
const os = await import("os");
|
|
31
|
+
const host = os.hostname();
|
|
32
|
+
const user = os.userInfo().username;
|
|
33
|
+
|
|
34
|
+
const { getMachineFingerprint, _resetFingerprintCache } = await import("../fingerprint");
|
|
35
|
+
_resetFingerprintCache();
|
|
36
|
+
const fp = getMachineFingerprint();
|
|
37
|
+
|
|
38
|
+
expect(fp).not.toContain(host);
|
|
39
|
+
expect(fp).not.toContain(user);
|
|
40
|
+
expect(fp).not.toMatch(/[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}/i); // no MAC pattern
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Note: an ideal test here would mock os.hostname() to verify the
|
|
44
|
+
// fingerprint changes when hostname changes, but Node's "os" module
|
|
45
|
+
// exports are non-configurable in ESM and vi.spyOn throws
|
|
46
|
+
// "Cannot redefine property: hostname". The behavior is covered
|
|
47
|
+
// indirectly by the memoization and composition tests above.
|
|
48
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
|
|
7
|
+
let tempDir: string;
|
|
8
|
+
|
|
9
|
+
function runGit(args: string[], cwd: string) {
|
|
10
|
+
execFileSync("git", args, { cwd, stdio: "pipe" });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tempDir = mkdtempSync(join(tmpdir(), "stagent-git-ops-"));
|
|
15
|
+
runGit(["init", "-b", "main"], tempDir);
|
|
16
|
+
runGit(["config", "user.email", "test@example.com"], tempDir);
|
|
17
|
+
runGit(["config", "user.name", "Test"], tempDir);
|
|
18
|
+
writeFileSync(join(tempDir, "README.md"), "# test\n");
|
|
19
|
+
runGit(["add", "README.md"], tempDir);
|
|
20
|
+
runGit(["commit", "-m", "initial"], tempDir);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("RealGitOps", () => {
|
|
28
|
+
it("isGitRepo returns true in a real repo", async () => {
|
|
29
|
+
const { createGitOps } = await import("../git-ops");
|
|
30
|
+
const ops = createGitOps(tempDir);
|
|
31
|
+
expect(ops.isGitRepo()).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("isGitRepo returns false outside a git repo", async () => {
|
|
35
|
+
const nonRepo = mkdtempSync(join(tmpdir(), "stagent-nogit-"));
|
|
36
|
+
try {
|
|
37
|
+
const { createGitOps } = await import("../git-ops");
|
|
38
|
+
const ops = createGitOps(nonRepo);
|
|
39
|
+
expect(ops.isGitRepo()).toBe(false);
|
|
40
|
+
} finally {
|
|
41
|
+
rmSync(nonRepo, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("getCurrentBranch returns main after init", async () => {
|
|
46
|
+
const { createGitOps } = await import("../git-ops");
|
|
47
|
+
const ops = createGitOps(tempDir);
|
|
48
|
+
expect(ops.getCurrentBranch()).toBe("main");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("branchExists returns true for main, false for missing", async () => {
|
|
52
|
+
const { createGitOps } = await import("../git-ops");
|
|
53
|
+
const ops = createGitOps(tempDir);
|
|
54
|
+
expect(ops.branchExists("main")).toBe(true);
|
|
55
|
+
expect(ops.branchExists("local")).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("createAndCheckoutBranch creates local at current HEAD", async () => {
|
|
59
|
+
const { createGitOps } = await import("../git-ops");
|
|
60
|
+
const ops = createGitOps(tempDir);
|
|
61
|
+
const mainSha = execFileSync("git", ["rev-parse", "main"], { cwd: tempDir, encoding: "utf-8" }).trim();
|
|
62
|
+
ops.createAndCheckoutBranch("local");
|
|
63
|
+
expect(ops.getCurrentBranch()).toBe("local");
|
|
64
|
+
expect(ops.branchExists("local")).toBe(true);
|
|
65
|
+
const localSha = execFileSync("git", ["rev-parse", "local"], { cwd: tempDir, encoding: "utf-8" }).trim();
|
|
66
|
+
expect(localSha).toBe(mainSha);
|
|
67
|
+
// main is not modified
|
|
68
|
+
const mainShaAfter = execFileSync("git", ["rev-parse", "main"], { cwd: tempDir, encoding: "utf-8" }).trim();
|
|
69
|
+
expect(mainShaAfter).toBe(mainSha);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("setConfig writes branch.local.pushRemote", async () => {
|
|
73
|
+
const { createGitOps } = await import("../git-ops");
|
|
74
|
+
const ops = createGitOps(tempDir);
|
|
75
|
+
ops.createAndCheckoutBranch("local");
|
|
76
|
+
ops.setConfig("branch.local.pushRemote", "no_push");
|
|
77
|
+
const value = execFileSync("git", ["config", "--get", "branch.local.pushRemote"], { cwd: tempDir, encoding: "utf-8" }).trim();
|
|
78
|
+
expect(value).toBe("no_push");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("getGitDir returns absolute path to .git directory", async () => {
|
|
82
|
+
const { createGitOps } = await import("../git-ops");
|
|
83
|
+
const ops = createGitOps(tempDir);
|
|
84
|
+
expect(ops.getGitDir()).toBe(join(tempDir, ".git"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("getCurrentBranch returns null when HEAD is detached", async () => {
|
|
88
|
+
// Detach HEAD by checking out the commit SHA directly
|
|
89
|
+
const sha = execFileSync("git", ["rev-parse", "HEAD"], { cwd: tempDir, encoding: "utf-8" }).trim();
|
|
90
|
+
execFileSync("git", ["checkout", sha], { cwd: tempDir, stdio: "pipe" });
|
|
91
|
+
const { createGitOps } = await import("../git-ops");
|
|
92
|
+
const ops = createGitOps(tempDir);
|
|
93
|
+
expect(ops.getCurrentBranch()).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
|
|
6
|
+
let tempDir: string;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tempDir = mkdtempSync(join(tmpdir(), "stagent-instance-settings-"));
|
|
10
|
+
vi.resetModules();
|
|
11
|
+
vi.stubEnv("STAGENT_DATA_DIR", tempDir);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.unstubAllEnvs();
|
|
16
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
async function loadModule() {
|
|
20
|
+
return await import("../settings");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("getInstanceConfig / setInstanceConfig", () => {
|
|
24
|
+
it("returns null before any config is written", async () => {
|
|
25
|
+
const { getInstanceConfig } = await loadModule();
|
|
26
|
+
expect(getInstanceConfig()).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("round-trips a config through set/get", async () => {
|
|
30
|
+
const { setInstanceConfig, getInstanceConfig } = await loadModule();
|
|
31
|
+
await setInstanceConfig({
|
|
32
|
+
instanceId: "abc-123",
|
|
33
|
+
branchName: "local",
|
|
34
|
+
isPrivateInstance: false,
|
|
35
|
+
createdAt: 1700000000,
|
|
36
|
+
});
|
|
37
|
+
const config = getInstanceConfig();
|
|
38
|
+
expect(config).toEqual({
|
|
39
|
+
instanceId: "abc-123",
|
|
40
|
+
branchName: "local",
|
|
41
|
+
isPrivateInstance: false,
|
|
42
|
+
createdAt: 1700000000,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns null when stored value is corrupt JSON", async () => {
|
|
47
|
+
const { setSetting } = await import("@/lib/settings/helpers");
|
|
48
|
+
await setSetting("instance", "not-valid-json");
|
|
49
|
+
const { getInstanceConfig } = await loadModule();
|
|
50
|
+
expect(getInstanceConfig()).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("getGuardrails / setGuardrails", () => {
|
|
55
|
+
it("returns defaults before any guardrails are written", async () => {
|
|
56
|
+
const { getGuardrails } = await loadModule();
|
|
57
|
+
expect(getGuardrails()).toEqual({
|
|
58
|
+
prePushHookInstalled: false,
|
|
59
|
+
prePushHookVersion: "",
|
|
60
|
+
pushRemoteBlocked: [],
|
|
61
|
+
consentStatus: "not_yet",
|
|
62
|
+
firstBootCompletedAt: null,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("round-trips guardrails through set/get", async () => {
|
|
67
|
+
const { setGuardrails, getGuardrails } = await loadModule();
|
|
68
|
+
await setGuardrails({
|
|
69
|
+
prePushHookInstalled: true,
|
|
70
|
+
prePushHookVersion: "1.0.0",
|
|
71
|
+
pushRemoteBlocked: ["local", "wealth-mgr"],
|
|
72
|
+
consentStatus: "enabled",
|
|
73
|
+
firstBootCompletedAt: 1700000000,
|
|
74
|
+
});
|
|
75
|
+
expect(getGuardrails()).toEqual({
|
|
76
|
+
prePushHookInstalled: true,
|
|
77
|
+
prePushHookVersion: "1.0.0",
|
|
78
|
+
pushRemoteBlocked: ["local", "wealth-mgr"],
|
|
79
|
+
consentStatus: "enabled",
|
|
80
|
+
firstBootCompletedAt: 1700000000,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
|
|
7
|
+
let tempDir: string;
|
|
8
|
+
let dataDir: string;
|
|
9
|
+
|
|
10
|
+
function runGit(args: string[], cwd: string) {
|
|
11
|
+
execFileSync("git", args, { cwd, stdio: "pipe" });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function initRepo(dir: string) {
|
|
15
|
+
runGit(["init", "-b", "main"], dir);
|
|
16
|
+
runGit(["config", "user.email", "test@example.com"], dir);
|
|
17
|
+
runGit(["config", "user.name", "Test"], dir);
|
|
18
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
19
|
+
runGit(["add", "README.md"], dir);
|
|
20
|
+
runGit(["commit", "-m", "initial"], dir);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
tempDir = mkdtempSync(join(tmpdir(), "stagent-upgrade-poller-"));
|
|
25
|
+
dataDir = mkdtempSync(join(tmpdir(), "stagent-upgrade-poller-data-"));
|
|
26
|
+
initRepo(tempDir);
|
|
27
|
+
vi.resetModules();
|
|
28
|
+
vi.unstubAllEnvs();
|
|
29
|
+
vi.stubEnv("STAGENT_DATA_DIR", dataDir);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.unstubAllEnvs();
|
|
34
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
35
|
+
rmSync(dataDir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("tick", () => {
|
|
39
|
+
it("returns skipped=dev_mode_or_no_git when STAGENT_DEV_MODE=true", async () => {
|
|
40
|
+
vi.stubEnv("STAGENT_DEV_MODE", "true");
|
|
41
|
+
const { tick } = await import("../upgrade-poller");
|
|
42
|
+
const result = await tick(tempDir);
|
|
43
|
+
expect(result.skipped).toBe("dev_mode_or_no_git");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns skipped=dev_mode_or_no_git when .git is absent", async () => {
|
|
47
|
+
const noGitDir = mkdtempSync(join(tmpdir(), "stagent-nogit-"));
|
|
48
|
+
try {
|
|
49
|
+
const { tick } = await import("../upgrade-poller");
|
|
50
|
+
const result = await tick(noGitDir);
|
|
51
|
+
expect(result.skipped).toBe("dev_mode_or_no_git");
|
|
52
|
+
} finally {
|
|
53
|
+
rmSync(noGitDir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns skipped=fetch_failed and records failure count when fetch fails (no remote)", async () => {
|
|
58
|
+
// No origin configured → fetch will fail
|
|
59
|
+
const { tick } = await import("../upgrade-poller");
|
|
60
|
+
const result = await tick(tempDir);
|
|
61
|
+
expect(result.skipped).toBe("fetch_failed");
|
|
62
|
+
expect(result.error).toBeDefined();
|
|
63
|
+
const { getUpgradeState } = await import("../settings");
|
|
64
|
+
const state = getUpgradeState();
|
|
65
|
+
expect(state.pollFailureCount).toBe(1);
|
|
66
|
+
expect(state.lastPollError).toBeTruthy();
|
|
67
|
+
expect(state.lastPolledAt).not.toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("increments pollFailureCount on repeated failures", async () => {
|
|
71
|
+
const { tick } = await import("../upgrade-poller");
|
|
72
|
+
await tick(tempDir);
|
|
73
|
+
await tick(tempDir);
|
|
74
|
+
await tick(tempDir);
|
|
75
|
+
const { getUpgradeState } = await import("../settings");
|
|
76
|
+
expect(getUpgradeState().pollFailureCount).toBe(3);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("successfully updates state with zero commitsBehind when local == origin/main", async () => {
|
|
80
|
+
// Set up a local 'origin' remote pointing to a bare copy of the same repo
|
|
81
|
+
const bareDir = mkdtempSync(join(tmpdir(), "stagent-bare-"));
|
|
82
|
+
try {
|
|
83
|
+
runGit(["init", "--bare", "-b", "main"], bareDir);
|
|
84
|
+
runGit(["remote", "add", "origin", bareDir], tempDir);
|
|
85
|
+
runGit(["push", "origin", "main"], tempDir);
|
|
86
|
+
|
|
87
|
+
const { tick } = await import("../upgrade-poller");
|
|
88
|
+
const result = await tick(tempDir);
|
|
89
|
+
expect(result.updated).toBeDefined();
|
|
90
|
+
expect(result.updated!.commitsBehind).toBe(0);
|
|
91
|
+
expect(result.updated!.upgradeAvailable).toBe(false);
|
|
92
|
+
expect(result.updated!.pollFailureCount).toBe(0);
|
|
93
|
+
expect(result.updated!.lastUpstreamSha).toBeTruthy();
|
|
94
|
+
expect(result.updated!.localMainSha).toBe(result.updated!.lastUpstreamSha);
|
|
95
|
+
} finally {
|
|
96
|
+
rmSync(bareDir, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("detects commits-behind when origin has new commits not in local main", async () => {
|
|
101
|
+
const bareDir = mkdtempSync(join(tmpdir(), "stagent-bare-"));
|
|
102
|
+
const otherCloneDir = mkdtempSync(join(tmpdir(), "stagent-other-"));
|
|
103
|
+
try {
|
|
104
|
+
runGit(["init", "--bare", "-b", "main"], bareDir);
|
|
105
|
+
runGit(["remote", "add", "origin", bareDir], tempDir);
|
|
106
|
+
runGit(["push", "origin", "main"], tempDir);
|
|
107
|
+
|
|
108
|
+
// Clone, add 2 commits, push back to bare
|
|
109
|
+
runGit(["clone", bareDir, otherCloneDir], otherCloneDir + "/..");
|
|
110
|
+
runGit(["config", "user.email", "test@example.com"], otherCloneDir);
|
|
111
|
+
runGit(["config", "user.name", "Test"], otherCloneDir);
|
|
112
|
+
writeFileSync(join(otherCloneDir, "a.txt"), "a\n");
|
|
113
|
+
runGit(["add", "a.txt"], otherCloneDir);
|
|
114
|
+
runGit(["commit", "-m", "a"], otherCloneDir);
|
|
115
|
+
writeFileSync(join(otherCloneDir, "b.txt"), "b\n");
|
|
116
|
+
runGit(["add", "b.txt"], otherCloneDir);
|
|
117
|
+
runGit(["commit", "-m", "b"], otherCloneDir);
|
|
118
|
+
runGit(["push", "origin", "main"], otherCloneDir);
|
|
119
|
+
|
|
120
|
+
const { tick } = await import("../upgrade-poller");
|
|
121
|
+
const result = await tick(tempDir);
|
|
122
|
+
expect(result.updated).toBeDefined();
|
|
123
|
+
expect(result.updated!.commitsBehind).toBe(2);
|
|
124
|
+
expect(result.updated!.upgradeAvailable).toBe(true);
|
|
125
|
+
expect(result.updated!.lastUpstreamSha).not.toBe(result.updated!.localMainSha);
|
|
126
|
+
} finally {
|
|
127
|
+
rmSync(bareDir, { recursive: true, force: true });
|
|
128
|
+
rmSync(otherCloneDir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|