@bugmole/cli 0.4.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/.bugmole.env.example +20 -0
- package/LICENSE +7 -0
- package/README.md +293 -0
- package/TESTING.md +117 -0
- package/bugmole.config.yaml +39 -0
- package/package.json +85 -0
- package/scripts/billing/paypal-setup.mjs +121 -0
- package/scripts/bugmole-continue.ts +318 -0
- package/scripts/bugmole-init.ts +188 -0
- package/scripts/bugmole.cjs +17 -0
- package/scripts/bugmole.test.ts +344 -0
- package/scripts/bugmole.ts +657 -0
- package/scripts/ensure-maestro.cjs +79 -0
- package/scripts/ios-tunnel-keeper.sh +45 -0
- package/scripts/ios-wda-keeper.sh +66 -0
- package/scripts/sync-plan-catalog.d.mts +3 -0
- package/scripts/sync-plan-catalog.mjs +16 -0
- package/scripts/ui-parity-diff.py +65 -0
- package/scripts/ui-parity-requirements.txt +1 -0
- package/scripts/verify-manage-to-plans.mts +194 -0
- package/spec/app-ui-audit.schema.json +176 -0
- package/spec/blockers.yaml +79 -0
- package/spec/bugs.index.json +42 -0
- package/spec/design-dna.schema.json +38 -0
- package/spec/domain_rules.yaml +24 -0
- package/spec/flows/flow_manage-to-plans-64c3c61b.yaml +8 -0
- package/spec/flows/flow_screen-to-evidence-6c3ab309.yaml +7 -0
- package/spec/journey_graph.yaml +126 -0
- package/spec/journeys.graph.json +2618 -0
- package/spec/plans/dashboard-smoke.flow.yaml +20 -0
- package/spec/plans/example.flow.yaml +99 -0
- package/spec/plans/flow_api-keys-to-logout-7396cd5d.flow.yaml +33 -0
- package/spec/plans/flow_api-keys-to-screen-157e10a6.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-audits-176c1eb8.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-blockers-633282c8.flow.yaml +112 -0
- package/spec/plans/flow_manage-to-devices-ad57e202.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-logout-86c54656.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-manage-resource-action-6325f99a.flow.yaml +185 -0
- package/spec/plans/flow_manage-to-parity-issues-01fa2579.flow.yaml +34 -0
- package/spec/plans/flow_manage-to-plans-64c3c61b.flow.yaml +123 -0
- package/spec/plans/flow_manage-to-screen-6857f915.flow.yaml +106 -0
- package/spec/plans/flow_manage-to-tasks-c5791c95.flow.yaml +33 -0
- package/spec/plans/flow_profile-to-screen-6045aa3d.flow.yaml +33 -0
- package/spec/plans/flow_register-to-api-auth-login-460a9cc8.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-audits-07666356.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-blockers-091d258b.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-devices-cad72f75.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-evidence-6c3ab309.flow.yaml +126 -0
- package/spec/plans/flow_screen-to-parity-issues-2218e3ad.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-plans-8232a94f.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-tasks-540671d1.flow.yaml +33 -0
- package/spec/plans/login.flow.yaml +22 -0
- package/spec/plans/owner-operations.flow.yaml +20 -0
- package/spec/project_config.yaml +55 -0
- package/spec/roles.yaml +30 -0
- package/spec/schema.md +394 -0
- package/spec/test-case-results.schema.json +62 -0
- package/spec/test-cases.schema.json +85 -0
- package/spec/ui-parity-audit.schema.json +194 -0
- package/spec/ui-reverse-engineering.schema.json +94 -0
- package/src/billing/plan-catalog.test.ts +46 -0
- package/src/billing/plan-catalog.ts +199 -0
- package/src/integrations/aws-sigv4.test.ts +42 -0
- package/src/integrations/aws-sigv4.ts +72 -0
- package/src/integrations/device-farm.ts +155 -0
- package/src/integrations/github-app.test.ts +57 -0
- package/src/integrations/github-app.ts +143 -0
- package/src/integrations/gitlab.ts +81 -0
- package/src/integrations/temp-email.test.ts +123 -0
- package/src/integrations/temp-email.ts +175 -0
- package/src/integrations/testflight-feedback.test.ts +51 -0
- package/src/integrations/testflight-feedback.ts +173 -0
- package/src/integrations/webdriver-client.ts +131 -0
- package/src/mcp/server.test.ts +1220 -0
- package/src/mcp/server.ts +3064 -0
- package/src/mcp/write-test-cases.test.ts +287 -0
- package/src/registry/api-key-client.ts +39 -0
- package/src/registry/control-plane-client.ts +212 -0
- package/src/registry/migrations/0001_registry.sql +47 -0
- package/src/registry/migrations/0002_device_authorizations.sql +23 -0
- package/src/registry/migrations/0003_project_environments.sql +25 -0
- package/src/registry/migrations/0004_device_authorization_email.sql +1 -0
- package/src/registry/migrations/0005_testing_control_plane.sql +55 -0
- package/src/registry/migrations/0006_workspaces.sql +36 -0
- package/src/registry/migrations/0007_project_apps.sql +26 -0
- package/src/registry/migrations/0008_agent_tasks.sql +30 -0
- package/src/registry/migrations/0009_journey_revisions.sql +17 -0
- package/src/registry/migrations/0010_agent_task_journey.sql +2 -0
- package/src/registry/migrations/0011_run_journey_revision.sql +1 -0
- package/src/registry/migrations/0012_canonical_flow_execution.sql +10 -0
- package/src/registry/migrations/0013_device_sessions.sql +22 -0
- package/src/registry/migrations/0014_agent_task_step.sql +1 -0
- package/src/registry/migrations/0015_agent_task_attempts.sql +6 -0
- package/src/registry/migrations/0016_github_issue_tracker.sql +54 -0
- package/src/registry/migrations/0017_run_targets.sql +7 -0
- package/src/registry/migrations/0018_run_fix_from.sql +4 -0
- package/src/registry/migrations/0019_workspace_flags.sql +9 -0
- package/src/registry/migrations/0020_orgs.sql +40 -0
- package/src/registry/migrations/0021_billing_core.sql +58 -0
- package/src/registry/migrations/0022_cloud_runners.sql +19 -0
- package/src/registry/migrations/0023_signup.sql +4 -0
- package/src/registry/migrations/0024_billing.sql +67 -0
- package/src/registry/migrations/0025_notifications.sql +47 -0
- package/src/registry/migrations/0026_repo_bindings.sql +28 -0
- package/src/registry/migrations/0027_feedback.sql +29 -0
- package/src/registry/migrations/0028_devices.sql +48 -0
- package/src/registry/migrations/0029_sso.sql +31 -0
- package/src/registry/migrations/0030_workspace_domains.sql +18 -0
- package/src/registry/migrations/0031_gitlab_and_teams.sql +45 -0
- package/src/registry/migrations/0032_personas.sql +15 -0
- package/src/registry/migrations/0033_bugmole_rename.sql +11 -0
- package/src/registry/task-scheduling.test.ts +100 -0
- package/src/registry/task-scheduling.ts +80 -0
- package/src/registry-worker/ai/platform-model.ts +77 -0
- package/src/registry-worker/ai/routes.test.ts +88 -0
- package/src/registry-worker/ai/routes.ts +90 -0
- package/src/registry-worker/artifacts.test.ts +98 -0
- package/src/registry-worker/artifacts.ts +85 -0
- package/src/registry-worker/billing/billing-core.test.ts +175 -0
- package/src/registry-worker/billing/checkout-routes.ts +209 -0
- package/src/registry-worker/billing/enforcement.ts +69 -0
- package/src/registry-worker/billing/entitlements.ts +108 -0
- package/src/registry-worker/billing/ledger.ts +186 -0
- package/src/registry-worker/billing/paypal/api.ts +259 -0
- package/src/registry-worker/billing/paypal/client.ts +91 -0
- package/src/registry-worker/billing/paypal/provider.ts +143 -0
- package/src/registry-worker/billing/paypal.test.ts +466 -0
- package/src/registry-worker/billing/provider.ts +114 -0
- package/src/registry-worker/billing/routes.ts +66 -0
- package/src/registry-worker/billing/subscriptions.ts +780 -0
- package/src/registry-worker/billing/thresholds.ts +107 -0
- package/src/registry-worker/core.ts +308 -0
- package/src/registry-worker/devices/devices.test.ts +185 -0
- package/src/registry-worker/devices/policy.ts +71 -0
- package/src/registry-worker/devices/routes.ts +453 -0
- package/src/registry-worker/domains/domains.test.ts +210 -0
- package/src/registry-worker/domains/routes.ts +139 -0
- package/src/registry-worker/domains.ts +88 -0
- package/src/registry-worker/email/sender.ts +75 -0
- package/src/registry-worker/env.d.ts +14716 -0
- package/src/registry-worker/features.ts +20 -0
- package/src/registry-worker/feedback/feedback.test.ts +230 -0
- package/src/registry-worker/feedback/format.ts +148 -0
- package/src/registry-worker/feedback/routes.ts +386 -0
- package/src/registry-worker/flags.ts +39 -0
- package/src/registry-worker/github/checks.test.ts +177 -0
- package/src/registry-worker/github/checks.ts +374 -0
- package/src/registry-worker/gitlab/checks.test.ts +141 -0
- package/src/registry-worker/gitlab/checks.ts +349 -0
- package/src/registry-worker/hooks.ts +54 -0
- package/src/registry-worker/index.ts +2077 -0
- package/src/registry-worker/jobs/index.ts +29 -0
- package/src/registry-worker/jobs/retention.ts +68 -0
- package/src/registry-worker/mcp/mcp.test.ts +355 -0
- package/src/registry-worker/mcp/routes.ts +215 -0
- package/src/registry-worker/mcp/token.ts +126 -0
- package/src/registry-worker/mcp/tools.ts +563 -0
- package/src/registry-worker/notifications/alerts.ts +212 -0
- package/src/registry-worker/notifications/notifications.test.ts +298 -0
- package/src/registry-worker/notifications/outbox.ts +83 -0
- package/src/registry-worker/notifications/routes.ts +280 -0
- package/src/registry-worker/notifications/secrets.ts +49 -0
- package/src/registry-worker/notifications/slack.ts +96 -0
- package/src/registry-worker/notifications/teams.ts +46 -0
- package/src/registry-worker/org/audit.ts +116 -0
- package/src/registry-worker/org/routes.test.ts +163 -0
- package/src/registry-worker/org/routes.ts +302 -0
- package/src/registry-worker/personas/personas.test.ts +78 -0
- package/src/registry-worker/personas/routes.ts +100 -0
- package/src/registry-worker/repo-triggers.ts +20 -0
- package/src/registry-worker/routes/index.ts +74 -0
- package/src/registry-worker/run-events.ts +24 -0
- package/src/registry-worker/runner/dispatch.ts +219 -0
- package/src/registry-worker/runner/jobs.ts +43 -0
- package/src/registry-worker/runner/metering.ts +82 -0
- package/src/registry-worker/runner/policy.ts +59 -0
- package/src/registry-worker/runner/routes.ts +171 -0
- package/src/registry-worker/runner/runner.test.ts +358 -0
- package/src/registry-worker/runner/tokens.ts +93 -0
- package/src/registry-worker/runs.test.ts +60 -0
- package/src/registry-worker/signup/policy.ts +57 -0
- package/src/registry-worker/signup/routes.ts +106 -0
- package/src/registry-worker/signup/signup.test.ts +81 -0
- package/src/registry-worker/sso/aegis.ts +141 -0
- package/src/registry-worker/sso/membership.ts +157 -0
- package/src/registry-worker/sso/routes.ts +458 -0
- package/src/registry-worker/sso/sso.test.ts +344 -0
- package/src/registry-worker/testing/d1-shim.ts +180 -0
- package/src/registry-worker/testing/harness.ts +137 -0
- package/src/runner-worker/index.ts +108 -0
- package/src/runtime/ai-analysis.ts +97 -0
- package/src/runtime/ai-exploration.test.ts +32 -0
- package/src/runtime/ai-exploration.ts +69 -0
- package/src/runtime/ai-repair.ts +74 -0
- package/src/runtime/ai-work.test.ts +99 -0
- package/src/runtime/android-screen-record.test.ts +75 -0
- package/src/runtime/android-screen-record.ts +192 -0
- package/src/runtime/app-understanding.test.ts +123 -0
- package/src/runtime/app-understanding.ts +201 -0
- package/src/runtime/appium-driver.test.ts +179 -0
- package/src/runtime/appium-driver.ts +295 -0
- package/src/runtime/blocker-resolution.test.ts +113 -0
- package/src/runtime/blocker-resolution.ts +111 -0
- package/src/runtime/browser-matrix.integration.test.ts +212 -0
- package/src/runtime/browser-matrix.test.ts +143 -0
- package/src/runtime/browser-matrix.ts +200 -0
- package/src/runtime/canonical-flow.test.ts +52 -0
- package/src/runtime/config-validate.ts +185 -0
- package/src/runtime/continuous-execution.ts +291 -0
- package/src/runtime/cursor-applescript.ts +573 -0
- package/src/runtime/cursor-cli-driver.test.ts +78 -0
- package/src/runtime/cursor-cli-driver.ts +156 -0
- package/src/runtime/cursor-driver-example.ts +117 -0
- package/src/runtime/cursor-driver-index.ts +65 -0
- package/src/runtime/cursor-driver-init.ts +277 -0
- package/src/runtime/cursor-driver-run.test.ts +15 -0
- package/src/runtime/cursor-driver-run.ts +323 -0
- package/src/runtime/cursor-driver.ts +332 -0
- package/src/runtime/cursor-llm-example.ts +90 -0
- package/src/runtime/cursor-llm.ts +206 -0
- package/src/runtime/cursor-mcp-monitor.ts +386 -0
- package/src/runtime/device-clouds/browserstack.ts +73 -0
- package/src/runtime/device-clouds/device-farm.ts +52 -0
- package/src/runtime/device-clouds/index.ts +92 -0
- package/src/runtime/device-clouds/kobiton.ts +70 -0
- package/src/runtime/device-clouds/targets.ts +44 -0
- package/src/runtime/device-clouds/types.ts +62 -0
- package/src/runtime/diff-proposal.ts +84 -0
- package/src/runtime/discovery-task.test.ts +29 -0
- package/src/runtime/discovery-task.ts +284 -0
- package/src/runtime/driver-recovery.ts +69 -0
- package/src/runtime/driver.ts +79 -0
- package/src/runtime/environment.test.ts +104 -0
- package/src/runtime/environment.ts +137 -0
- package/src/runtime/executor.test.ts +509 -0
- package/src/runtime/executor.ts +921 -0
- package/src/runtime/explorer.test.ts +101 -0
- package/src/runtime/explorer.ts +1013 -0
- package/src/runtime/failure-analysis.test.ts +111 -0
- package/src/runtime/failure-analysis.ts +272 -0
- package/src/runtime/fixtures/fake-maestro.sh +36 -0
- package/src/runtime/flow-language.test.ts +268 -0
- package/src/runtime/flow-language.ts +414 -0
- package/src/runtime/init-wizard.ts +354 -0
- package/src/runtime/ios-screen-record.test.ts +68 -0
- package/src/runtime/ios-screen-record.ts +155 -0
- package/src/runtime/journey-editor.ts +452 -0
- package/src/runtime/journey-evidence.test.ts +161 -0
- package/src/runtime/journey-evidence.ts +180 -0
- package/src/runtime/journey-graph.test.ts +257 -0
- package/src/runtime/journey-graph.ts +170 -0
- package/src/runtime/legacy-names.ts +32 -0
- package/src/runtime/llm-example.ts +105 -0
- package/src/runtime/llm.ts +527 -0
- package/src/runtime/local-browser.test.ts +45 -0
- package/src/runtime/local-browser.ts +48 -0
- package/src/runtime/local-registry-stub.test.ts +325 -0
- package/src/runtime/local-registry-stub.ts +803 -0
- package/src/runtime/maestro-driver.test.ts +84 -0
- package/src/runtime/maestro-driver.ts +209 -0
- package/src/runtime/mole-voice.ts +21 -0
- package/src/runtime/nav-crawl.test.ts +100 -0
- package/src/runtime/nav-crawl.ts +153 -0
- package/src/runtime/pipeline.test.ts +405 -0
- package/src/runtime/pipeline.ts +833 -0
- package/src/runtime/planner.test.ts +37 -0
- package/src/runtime/planner.ts +274 -0
- package/src/runtime/platform-ai.ts +76 -0
- package/src/runtime/playwright-driver.test.ts +93 -0
- package/src/runtime/playwright-driver.ts +620 -0
- package/src/runtime/project-spec.ts +140 -0
- package/src/runtime/record-run-verdicts.ts +68 -0
- package/src/runtime/reporter.test.ts +56 -0
- package/src/runtime/reporter.ts +158 -0
- package/src/runtime/reset.test.ts +44 -0
- package/src/runtime/reset.ts +61 -0
- package/src/runtime/reviewer.test.ts +73 -0
- package/src/runtime/reviewer.ts +158 -0
- package/src/runtime/run-job.ts +136 -0
- package/src/runtime/run-once.test.ts +207 -0
- package/src/runtime/run-once.ts +168 -0
- package/src/runtime/run.ts +132 -0
- package/src/runtime/screen-recording.ts +34 -0
- package/src/runtime/serve-gateway.test.ts +74 -0
- package/src/runtime/serve-gateway.ts +164 -0
- package/src/runtime/serve-worker.test.ts +23 -0
- package/src/runtime/serve-worker.ts +278 -0
- package/src/runtime/site-discovery.test.ts +168 -0
- package/src/runtime/site-discovery.ts +308 -0
- package/src/runtime/target-runner.ts +144 -0
- package/src/runtime/test-case-verdicts.test.ts +94 -0
- package/src/runtime/test-case-verdicts.ts +120 -0
- package/src/runtime/ui-reverse-engineering/coordinator.test.ts +59 -0
- package/src/runtime/ui-reverse-engineering/coordinator.ts +391 -0
- package/src/runtime/ui-reverse-engineering/types.ts +197 -0
- package/src/runtime/web-suite.test.ts +97 -0
- package/src/runtime/web-suite.ts +176 -0
- package/src/storage/create-object-store.ts +144 -0
- package/src/storage/keys.ts +34 -0
- package/src/storage/local-artifact-server.test.ts +314 -0
- package/src/storage/local-artifact-server.ts +357 -0
- package/src/storage/object-store.test.ts +28 -0
- package/src/storage/object-store.ts +101 -0
- package/src/storage/registry-object-store.ts +88 -0
- package/src/storage/remote-object-store.ts +104 -0
- package/src/storage/storage-directory.test.ts +43 -0
- package/src/storage/storage-directory.ts +24 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { callToolInternal } from "./server.js";
|
|
7
|
+
|
|
8
|
+
async function setup() {
|
|
9
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "bugmole-test-cases-"));
|
|
10
|
+
const specDir = path.join(directory, "spec");
|
|
11
|
+
await fs.mkdir(specDir, { recursive: true });
|
|
12
|
+
// A real journey graph is the grounding these assumptions get checked against.
|
|
13
|
+
await fs.writeFile(
|
|
14
|
+
path.join(specDir, "journeys.graph.json"),
|
|
15
|
+
JSON.stringify({
|
|
16
|
+
version: 2,
|
|
17
|
+
projectId: "demo",
|
|
18
|
+
nodes: [
|
|
19
|
+
{ id: "screen:/", title: "Home", kind: "screen", route: "/" },
|
|
20
|
+
{ id: "screen:/login", title: "Login", kind: "screen", route: "/login" },
|
|
21
|
+
],
|
|
22
|
+
edges: [{ id: "screen:/->screen:/login:1", from: "screen:/", to: "screen:/login" }],
|
|
23
|
+
journeys: [{
|
|
24
|
+
id: "flow:login",
|
|
25
|
+
name: "Login",
|
|
26
|
+
actor: "user",
|
|
27
|
+
nodeIds: ["screen:/", "screen:/login"],
|
|
28
|
+
edgeIds: ["screen:/->screen:/login:1"],
|
|
29
|
+
revision: 2,
|
|
30
|
+
status: "draft",
|
|
31
|
+
}],
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
const cfg = {
|
|
35
|
+
project: { id: "demo" },
|
|
36
|
+
runtime: { spec_dir: specDir, artifacts_dir: directory },
|
|
37
|
+
storage: { provider: "local", localDirectory: directory },
|
|
38
|
+
};
|
|
39
|
+
return { directory, cfg };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const validCase = {
|
|
43
|
+
caseId: "TC-001",
|
|
44
|
+
title: "Login link reaches the login screen",
|
|
45
|
+
nodeId: "screen:/login",
|
|
46
|
+
edgeId: "screen:/->screen:/login:1",
|
|
47
|
+
intent: "Prove the home page can reach login",
|
|
48
|
+
steps: ["Open /", "Click the login link"],
|
|
49
|
+
expected: ["The /login screen renders"],
|
|
50
|
+
confidence: "medium",
|
|
51
|
+
basis: ["src/app/page.tsx has a link to /login"],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
test("writes a grounded case set and reports where it landed", async () => {
|
|
55
|
+
const { directory, cfg } = await setup();
|
|
56
|
+
try {
|
|
57
|
+
const result = await callToolInternal("bugmole_write_test_cases", {
|
|
58
|
+
caseSet: {
|
|
59
|
+
schemaVersion: 1,
|
|
60
|
+
caseSetId: "flow-login",
|
|
61
|
+
journeyId: "flow:login",
|
|
62
|
+
journeyRevision: 2,
|
|
63
|
+
cases: [validCase],
|
|
64
|
+
},
|
|
65
|
+
}, cfg);
|
|
66
|
+
assert.equal(result.status, "success");
|
|
67
|
+
assert.equal(result.caseCount, 1);
|
|
68
|
+
const written = JSON.parse(
|
|
69
|
+
await fs.readFile(path.join(directory, "projects", "demo", "test-cases", "flow-login.json"), "utf8"),
|
|
70
|
+
);
|
|
71
|
+
assert.equal(written.cases[0].caseId, "TC-001");
|
|
72
|
+
assert.equal(written.projectId, "demo");
|
|
73
|
+
} finally {
|
|
74
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("refuses a case pinned to a step that is not in the journey", async () => {
|
|
79
|
+
const { directory, cfg } = await setup();
|
|
80
|
+
try {
|
|
81
|
+
await assert.rejects(
|
|
82
|
+
callToolInternal("bugmole_write_test_cases", {
|
|
83
|
+
caseSet: {
|
|
84
|
+
schemaVersion: 1,
|
|
85
|
+
caseSetId: "bad-node",
|
|
86
|
+
journeyId: "flow:login",
|
|
87
|
+
journeyRevision: 2,
|
|
88
|
+
cases: [{ ...validCase, nodeId: "screen:/does-not-exist", edgeId: undefined }],
|
|
89
|
+
},
|
|
90
|
+
}, cfg),
|
|
91
|
+
/is not a step of journey/,
|
|
92
|
+
);
|
|
93
|
+
} finally {
|
|
94
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("refuses a stale journey revision rather than attaching cases to the wrong version", async () => {
|
|
99
|
+
const { directory, cfg } = await setup();
|
|
100
|
+
try {
|
|
101
|
+
await assert.rejects(
|
|
102
|
+
callToolInternal("bugmole_write_test_cases", {
|
|
103
|
+
caseSet: {
|
|
104
|
+
schemaVersion: 1,
|
|
105
|
+
caseSetId: "stale",
|
|
106
|
+
journeyId: "flow:login",
|
|
107
|
+
journeyRevision: 1,
|
|
108
|
+
cases: [validCase],
|
|
109
|
+
},
|
|
110
|
+
}, cfg),
|
|
111
|
+
/journeyRevision must match/,
|
|
112
|
+
);
|
|
113
|
+
} finally {
|
|
114
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a case with no basis cannot claim more than low confidence", async () => {
|
|
119
|
+
const { directory, cfg } = await setup();
|
|
120
|
+
try {
|
|
121
|
+
await assert.rejects(
|
|
122
|
+
callToolInternal("bugmole_write_test_cases", {
|
|
123
|
+
caseSet: {
|
|
124
|
+
schemaVersion: 1,
|
|
125
|
+
caseSetId: "unfounded",
|
|
126
|
+
journeyId: "flow:login",
|
|
127
|
+
journeyRevision: 2,
|
|
128
|
+
cases: [{ ...validCase, basis: [], confidence: "high" }],
|
|
129
|
+
},
|
|
130
|
+
}, cfg),
|
|
131
|
+
/must be confidence "low"/,
|
|
132
|
+
);
|
|
133
|
+
} finally {
|
|
134
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("an honest low-confidence guess is allowed through, so gaps stay visible", async () => {
|
|
139
|
+
const { directory, cfg } = await setup();
|
|
140
|
+
try {
|
|
141
|
+
const result = await callToolInternal("bugmole_write_test_cases", {
|
|
142
|
+
caseSet: {
|
|
143
|
+
schemaVersion: 1,
|
|
144
|
+
caseSetId: "guess",
|
|
145
|
+
journeyId: "flow:login",
|
|
146
|
+
journeyRevision: 2,
|
|
147
|
+
cases: [{ ...validCase, basis: [], confidence: "low" }],
|
|
148
|
+
},
|
|
149
|
+
}, cfg);
|
|
150
|
+
assert.equal(result.status, "success");
|
|
151
|
+
} finally {
|
|
152
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("rejects duplicate case ids within one set", async () => {
|
|
157
|
+
const { directory, cfg } = await setup();
|
|
158
|
+
try {
|
|
159
|
+
await assert.rejects(
|
|
160
|
+
callToolInternal("bugmole_write_test_cases", {
|
|
161
|
+
caseSet: {
|
|
162
|
+
schemaVersion: 1,
|
|
163
|
+
caseSetId: "dupes",
|
|
164
|
+
journeyId: "flow:login",
|
|
165
|
+
journeyRevision: 2,
|
|
166
|
+
cases: [validCase, { ...validCase, title: "Another" }],
|
|
167
|
+
},
|
|
168
|
+
}, cfg),
|
|
169
|
+
/duplicate caseId/,
|
|
170
|
+
);
|
|
171
|
+
} finally {
|
|
172
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
async function withCaseSet(cfg: any) {
|
|
177
|
+
await callToolInternal("bugmole_write_test_cases", {
|
|
178
|
+
caseSet: {
|
|
179
|
+
schemaVersion: 1,
|
|
180
|
+
caseSetId: "flow-login",
|
|
181
|
+
journeyId: "flow:login",
|
|
182
|
+
journeyRevision: 2,
|
|
183
|
+
cases: [validCase],
|
|
184
|
+
},
|
|
185
|
+
}, cfg);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
test("records a run's verdict against a case set that exists", async () => {
|
|
189
|
+
const { directory, cfg } = await setup();
|
|
190
|
+
try {
|
|
191
|
+
await withCaseSet(cfg);
|
|
192
|
+
const result = await callToolInternal("bugmole_write_test_case_results", {
|
|
193
|
+
resultSet: {
|
|
194
|
+
schemaVersion: 1,
|
|
195
|
+
caseSetId: "flow-login",
|
|
196
|
+
results: [{ caseId: "TC-001", status: "passed", detail: "200 with the login form present" }],
|
|
197
|
+
},
|
|
198
|
+
}, cfg);
|
|
199
|
+
assert.equal(result.status, "success");
|
|
200
|
+
assert.deepEqual(result.counts, { passed: 1 });
|
|
201
|
+
const written = JSON.parse(
|
|
202
|
+
await fs.readFile(path.join(directory, "projects", "demo", "test-case-results", "flow-login.json"), "utf8"),
|
|
203
|
+
);
|
|
204
|
+
assert.equal(written.results[0].status, "passed");
|
|
205
|
+
// Stamped from the case set, so a verdict always says which revision it judged.
|
|
206
|
+
assert.equal(written.journeyRevision, 2);
|
|
207
|
+
assert.equal(written.journeyId, "flow:login");
|
|
208
|
+
} finally {
|
|
209
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("refuses a verdict for a case nobody ever wrote", async () => {
|
|
214
|
+
const { directory, cfg } = await setup();
|
|
215
|
+
try {
|
|
216
|
+
await withCaseSet(cfg);
|
|
217
|
+
await assert.rejects(
|
|
218
|
+
callToolInternal("bugmole_write_test_case_results", {
|
|
219
|
+
resultSet: {
|
|
220
|
+
schemaVersion: 1,
|
|
221
|
+
caseSetId: "flow-login",
|
|
222
|
+
results: [{ caseId: "TC-INVENTED", status: "passed", detail: "looked fine" }],
|
|
223
|
+
},
|
|
224
|
+
}, cfg),
|
|
225
|
+
/is not a case of flow-login/,
|
|
226
|
+
);
|
|
227
|
+
} finally {
|
|
228
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("refuses results for a case set that does not exist yet", async () => {
|
|
233
|
+
const { directory, cfg } = await setup();
|
|
234
|
+
try {
|
|
235
|
+
await assert.rejects(
|
|
236
|
+
callToolInternal("bugmole_write_test_case_results", {
|
|
237
|
+
resultSet: {
|
|
238
|
+
schemaVersion: 1,
|
|
239
|
+
caseSetId: "flow-login",
|
|
240
|
+
results: [{ caseId: "TC-001", status: "passed", detail: "fine" }],
|
|
241
|
+
},
|
|
242
|
+
}, cfg),
|
|
243
|
+
/No test cases found/,
|
|
244
|
+
);
|
|
245
|
+
} finally {
|
|
246
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("refuses a pass with no detail, which is the shape of a fabricated result", async () => {
|
|
251
|
+
const { directory, cfg } = await setup();
|
|
252
|
+
try {
|
|
253
|
+
await withCaseSet(cfg);
|
|
254
|
+
await assert.rejects(
|
|
255
|
+
callToolInternal("bugmole_write_test_case_results", {
|
|
256
|
+
resultSet: {
|
|
257
|
+
schemaVersion: 1,
|
|
258
|
+
caseSetId: "flow-login",
|
|
259
|
+
results: [{ caseId: "TC-001", status: "passed" }],
|
|
260
|
+
},
|
|
261
|
+
}, cfg),
|
|
262
|
+
/detail is required/,
|
|
263
|
+
);
|
|
264
|
+
} finally {
|
|
265
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("refuses verdicts recorded against a stale journey revision", async () => {
|
|
270
|
+
const { directory, cfg } = await setup();
|
|
271
|
+
try {
|
|
272
|
+
await withCaseSet(cfg);
|
|
273
|
+
await assert.rejects(
|
|
274
|
+
callToolInternal("bugmole_write_test_case_results", {
|
|
275
|
+
resultSet: {
|
|
276
|
+
schemaVersion: 1,
|
|
277
|
+
caseSetId: "flow-login",
|
|
278
|
+
journeyRevision: 1,
|
|
279
|
+
results: [{ caseId: "TC-001", status: "passed", detail: "observed" }],
|
|
280
|
+
},
|
|
281
|
+
}, cfg),
|
|
282
|
+
/does not match the case set revision/,
|
|
283
|
+
);
|
|
284
|
+
} finally {
|
|
285
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type CreateApiKeyOptions = {
|
|
2
|
+
registryUrl: string;
|
|
3
|
+
aegisId?: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
aegisSecret?: string;
|
|
6
|
+
name: string;
|
|
7
|
+
scopes?: string[];
|
|
8
|
+
projectIds?: string[];
|
|
9
|
+
expiresAt?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function createRemoteApiKey(options: CreateApiKeyOptions): Promise<{
|
|
13
|
+
id: string;
|
|
14
|
+
token: string;
|
|
15
|
+
expiresAt: string | null;
|
|
16
|
+
}> {
|
|
17
|
+
const response = await fetch(`${options.registryUrl.replace(/\/$/, "")}/api/api-keys`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
"content-type": "application/json",
|
|
21
|
+
...(options.apiKey
|
|
22
|
+
? { authorization: `Bearer ${options.apiKey}` }
|
|
23
|
+
: {
|
|
24
|
+
"x-aegis-id": options.aegisId ?? "",
|
|
25
|
+
...(options.aegisSecret ? { "x-aegis-secret": options.aegisSecret } : {}),
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
name: options.name,
|
|
30
|
+
scopes: options.scopes,
|
|
31
|
+
projectIds: options.projectIds,
|
|
32
|
+
expiresAt: options.expiresAt,
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`Bugmole registry rejected API key creation (${response.status})`);
|
|
37
|
+
}
|
|
38
|
+
return response.json() as Promise<{ id: string; token: string; expiresAt: string | null }>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
|
|
3
|
+
export type RegistryRun = {
|
|
4
|
+
id: string;
|
|
5
|
+
projectId: string;
|
|
6
|
+
planId: string;
|
|
7
|
+
journeyId?: string;
|
|
8
|
+
status: string;
|
|
9
|
+
driver?: string;
|
|
10
|
+
platform?: string;
|
|
11
|
+
recoveryAttempts?: number;
|
|
12
|
+
failureCode?: string;
|
|
13
|
+
artifactSummaryJson?: string;
|
|
14
|
+
environmentId?: string | null;
|
|
15
|
+
/** The environment's base URL, snapshotted when the run was requested. */
|
|
16
|
+
environmentBaseUrl?: string | null;
|
|
17
|
+
/** Requested browser/device matrix. */
|
|
18
|
+
targets?: Array<{ browser: string; device?: string }> | null;
|
|
19
|
+
targetResults?: Array<Record<string, unknown>> | null;
|
|
20
|
+
/** Re-run with the fix suggested by this earlier run/target. */
|
|
21
|
+
fixFrom?: { runId: string; targetId: string; mode?: "suggested" | "ai" } | null;
|
|
22
|
+
durationMs?: number | null;
|
|
23
|
+
/** "cloud" runs belong to Bugmole's hosted runners; a customer's worker skips them. */
|
|
24
|
+
executionMode?: "self" | "cloud";
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type RegistryTask = {
|
|
28
|
+
id: string;
|
|
29
|
+
projectId: string;
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
taskType: "code" | "ui" | "investigation" | "discovery";
|
|
33
|
+
priority: number;
|
|
34
|
+
status: string;
|
|
35
|
+
workerId?: string;
|
|
36
|
+
progressMessage?: string;
|
|
37
|
+
parityMatchScore?: number;
|
|
38
|
+
parityAuditId?: string;
|
|
39
|
+
journeyId?: string;
|
|
40
|
+
journeyRevision?: number;
|
|
41
|
+
stepId?: string;
|
|
42
|
+
idempotencyKey?: string;
|
|
43
|
+
attempts?: number;
|
|
44
|
+
nextAttemptAt?: string | null;
|
|
45
|
+
leaseExpiresAt?: string | null;
|
|
46
|
+
/** "cloud" discovery belongs to Bugmole's hosted runners; a customer's worker skips it. */
|
|
47
|
+
executionMode?: "self" | "cloud";
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export { MAX_TASK_ATTEMPTS, backoffUntil, isClaimableTask } from "./task-scheduling.js";
|
|
51
|
+
|
|
52
|
+
export type ControlPlaneClientOptions = {
|
|
53
|
+
registryUrl: string;
|
|
54
|
+
apiKey: string;
|
|
55
|
+
projectId: string;
|
|
56
|
+
workspaceId?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Carries the HTTP status, so callers can tell a lost claim (409) from a real failure. */
|
|
60
|
+
export class RegistryRequestError extends Error {
|
|
61
|
+
constructor(readonly status: number) {
|
|
62
|
+
super(`Registry request failed (${status})`);
|
|
63
|
+
this.name = "RegistryRequestError";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class ControlPlaneClient {
|
|
68
|
+
private readonly baseUrl: string;
|
|
69
|
+
|
|
70
|
+
constructor(private readonly options: ControlPlaneClientOptions) {
|
|
71
|
+
this.baseUrl = options.registryUrl.replace(/\/$/, "");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
75
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
76
|
+
...init,
|
|
77
|
+
headers: {
|
|
78
|
+
authorization: `Bearer ${this.options.apiKey}`,
|
|
79
|
+
"content-type": "application/json",
|
|
80
|
+
...(init.headers ?? {}),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const body = await response.json().catch(() => ({}));
|
|
84
|
+
if (!response.ok) throw new RegistryRequestError(response.status);
|
|
85
|
+
return body as T;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async publishPlan(input: { planId: string; journeyId: string; objectKey: string }) {
|
|
89
|
+
return this.request<{ plan: unknown }>(`/api/projects/${encodeURIComponent(this.options.projectId)}/plans`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
body: JSON.stringify(input),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async listRuns(): Promise<{ runs: RegistryRun[] }> {
|
|
96
|
+
return this.request(`/api/projects/${encodeURIComponent(this.options.projectId)}/runs`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async createRun(input: {
|
|
100
|
+
planId: string;
|
|
101
|
+
idempotencyKey: string;
|
|
102
|
+
journeyRevision?: number;
|
|
103
|
+
environmentId?: string;
|
|
104
|
+
driver?: string;
|
|
105
|
+
platform?: string;
|
|
106
|
+
targets?: Array<{ browser: string; device?: string }>;
|
|
107
|
+
}): Promise<{ run: RegistryRun }> {
|
|
108
|
+
return this.request(`/api/projects/${encodeURIComponent(this.options.projectId)}/runs`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
body: JSON.stringify(input),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async listPlans(): Promise<{ plans: Array<{ planId: string; journeyId?: string; status?: string }> }> {
|
|
115
|
+
return this.request(`/api/projects/${encodeURIComponent(this.options.projectId)}/plans`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async claimRun(runId: string, workerId: string) {
|
|
119
|
+
return this.request(`/api/runs/${encodeURIComponent(runId)}/claim`, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
body: JSON.stringify({ workerId }),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async updateRun(runId: string, update: Record<string, unknown>) {
|
|
126
|
+
return this.request(`/api/runs/${encodeURIComponent(runId)}`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
body: JSON.stringify(update),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async listTasks(): Promise<{ tasks: RegistryTask[] }> {
|
|
133
|
+
return this.request(`/api/projects/${encodeURIComponent(this.options.projectId)}/tasks`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async createTask(input: {
|
|
137
|
+
title: string;
|
|
138
|
+
description: string;
|
|
139
|
+
taskType?: "code" | "ui" | "investigation" | "discovery";
|
|
140
|
+
priority?: number;
|
|
141
|
+
idempotencyKey: string;
|
|
142
|
+
journeyId?: string;
|
|
143
|
+
journeyRevision?: number;
|
|
144
|
+
}) {
|
|
145
|
+
return this.request<{ task: RegistryTask }>(
|
|
146
|
+
`/api/projects/${encodeURIComponent(this.options.projectId)}/tasks`,
|
|
147
|
+
{ method: "POST", body: JSON.stringify(input) },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async claimTask(taskId: string, workerId: string): Promise<{ claimed: boolean; task: RegistryTask }> {
|
|
152
|
+
return this.request(`/api/tasks/${encodeURIComponent(taskId)}/claim`, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
body: JSON.stringify({ workerId }),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async updateTask(taskId: string, update: Record<string, unknown>) {
|
|
159
|
+
return this.request<{ updated: boolean; task: RegistryTask }>(`/api/tasks/${encodeURIComponent(taskId)}`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
body: JSON.stringify(update),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async listJourneyRevisions() {
|
|
166
|
+
return this.request<{ revisions: Array<Record<string, unknown>> }>(
|
|
167
|
+
`/api/projects/${encodeURIComponent(this.options.projectId)}/journeys`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async saveJourneyRevision(input: {
|
|
172
|
+
journeyId: string;
|
|
173
|
+
objectKey: string;
|
|
174
|
+
expectedRevision?: number;
|
|
175
|
+
appId?: string;
|
|
176
|
+
environmentId?: string;
|
|
177
|
+
status?: "draft" | "published";
|
|
178
|
+
sourceOfTruth?: "canonical_flow";
|
|
179
|
+
stability?: "discovered" | "stabilizing" | "stable";
|
|
180
|
+
linkedToGraph?: boolean;
|
|
181
|
+
verificationRunId?: string;
|
|
182
|
+
}) {
|
|
183
|
+
return this.request(`/api/projects/${encodeURIComponent(this.options.projectId)}/journeys`, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
body: JSON.stringify(input),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
connectWorker(workerId: string, onMessage: (message: Record<string, unknown>) => void): WebSocket {
|
|
190
|
+
const url = `${this.baseUrl.replace(/^http/, "ws")}/api/ws/projects/${encodeURIComponent(this.options.projectId)}`;
|
|
191
|
+
const socket = new WebSocket(url, {
|
|
192
|
+
headers: { authorization: `Bearer ${this.options.apiKey}` },
|
|
193
|
+
});
|
|
194
|
+
socket.on("open", () => {
|
|
195
|
+
socket.send(JSON.stringify({
|
|
196
|
+
type: "worker_hello",
|
|
197
|
+
workerId,
|
|
198
|
+
projectId: this.options.projectId,
|
|
199
|
+
workspaceId: this.options.workspaceId,
|
|
200
|
+
}));
|
|
201
|
+
});
|
|
202
|
+
socket.on("message", (raw) => {
|
|
203
|
+
try {
|
|
204
|
+
const message = JSON.parse(raw.toString()) as Record<string, unknown>;
|
|
205
|
+
onMessage(message);
|
|
206
|
+
} catch {
|
|
207
|
+
// Ignore malformed coordinator messages.
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
return socket;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS aegis_users (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
4
|
+
);
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
7
|
+
id TEXT PRIMARY KEY,
|
|
8
|
+
tenant_id TEXT NOT NULL,
|
|
9
|
+
name TEXT NOT NULL,
|
|
10
|
+
environment TEXT NOT NULL,
|
|
11
|
+
storage_provider TEXT NOT NULL,
|
|
12
|
+
created_at TEXT NOT NULL
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE TABLE IF NOT EXISTS project_memberships (
|
|
16
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
17
|
+
aegis_id TEXT NOT NULL REFERENCES aegis_users(id) ON DELETE CASCADE,
|
|
18
|
+
role TEXT NOT NULL,
|
|
19
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
20
|
+
PRIMARY KEY (project_id, aegis_id)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS aegis_claims (
|
|
24
|
+
token_hash TEXT PRIMARY KEY,
|
|
25
|
+
temporary_aegis_id TEXT NOT NULL,
|
|
26
|
+
tenant_id TEXT NOT NULL,
|
|
27
|
+
expires_at TEXT NOT NULL,
|
|
28
|
+
claimed_at TEXT
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
32
|
+
id TEXT PRIMARY KEY,
|
|
33
|
+
aegis_id TEXT NOT NULL REFERENCES aegis_users(id) ON DELETE CASCADE,
|
|
34
|
+
name TEXT NOT NULL,
|
|
35
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
36
|
+
scopes_json TEXT NOT NULL,
|
|
37
|
+
project_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
38
|
+
expires_at TEXT,
|
|
39
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
40
|
+
revoked_at TEXT
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_project_memberships_aegis
|
|
44
|
+
ON project_memberships(aegis_id, status);
|
|
45
|
+
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_aegis
|
|
47
|
+
ON api_keys(aegis_id, revoked_at);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS device_authorizations (
|
|
2
|
+
attempt_id TEXT PRIMARY KEY,
|
|
3
|
+
code_hash TEXT NOT NULL,
|
|
4
|
+
poll_secret_hash TEXT NOT NULL,
|
|
5
|
+
project_id TEXT NOT NULL,
|
|
6
|
+
project_name TEXT NOT NULL,
|
|
7
|
+
environment TEXT NOT NULL,
|
|
8
|
+
storage_provider TEXT NOT NULL,
|
|
9
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
10
|
+
aegis_id TEXT,
|
|
11
|
+
api_key_id TEXT,
|
|
12
|
+
encrypted_token TEXT,
|
|
13
|
+
token_iv TEXT,
|
|
14
|
+
source_ip TEXT,
|
|
15
|
+
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
expires_at TEXT NOT NULL,
|
|
17
|
+
claimed_at TEXT,
|
|
18
|
+
completed_at TEXT,
|
|
19
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_device_authorizations_expiry
|
|
23
|
+
ON device_authorizations(status, expires_at);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS project_environments (
|
|
2
|
+
id TEXT NOT NULL,
|
|
3
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
4
|
+
name TEXT NOT NULL,
|
|
5
|
+
base_url TEXT NOT NULL,
|
|
6
|
+
api_url TEXT,
|
|
7
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
8
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
9
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
10
|
+
PRIMARY KEY (project_id, id)
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_project_environments_project
|
|
14
|
+
ON project_environments(project_id, status);
|
|
15
|
+
|
|
16
|
+
INSERT OR IGNORE INTO project_environments
|
|
17
|
+
(id, project_id, name, base_url, status, is_default)
|
|
18
|
+
SELECT
|
|
19
|
+
lower(replace(environment, ' ', '-')),
|
|
20
|
+
id,
|
|
21
|
+
environment,
|
|
22
|
+
'',
|
|
23
|
+
'active',
|
|
24
|
+
1
|
|
25
|
+
FROM projects;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE device_authorizations ADD COLUMN aegis_email TEXT;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS plan_drafts (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
4
|
+
tenant_id TEXT NOT NULL,
|
|
5
|
+
plan_id TEXT NOT NULL,
|
|
6
|
+
journey_id TEXT NOT NULL,
|
|
7
|
+
object_key TEXT NOT NULL,
|
|
8
|
+
status TEXT NOT NULL DEFAULT 'draft',
|
|
9
|
+
created_by TEXT NOT NULL,
|
|
10
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
11
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
12
|
+
UNIQUE(project_id, plan_id)
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
16
|
+
id TEXT PRIMARY KEY,
|
|
17
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
18
|
+
tenant_id TEXT NOT NULL,
|
|
19
|
+
plan_id TEXT NOT NULL,
|
|
20
|
+
journey_id TEXT,
|
|
21
|
+
environment_id TEXT,
|
|
22
|
+
requested_by TEXT NOT NULL,
|
|
23
|
+
idempotency_key TEXT NOT NULL,
|
|
24
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
25
|
+
worker_id TEXT,
|
|
26
|
+
lease_expires_at TEXT,
|
|
27
|
+
heartbeat_at TEXT,
|
|
28
|
+
current_step TEXT,
|
|
29
|
+
steps_executed INTEGER NOT NULL DEFAULT 0,
|
|
30
|
+
steps_passed INTEGER NOT NULL DEFAULT 0,
|
|
31
|
+
steps_failed INTEGER NOT NULL DEFAULT 0,
|
|
32
|
+
error_code TEXT,
|
|
33
|
+
error_message TEXT,
|
|
34
|
+
manifest_key TEXT,
|
|
35
|
+
requested_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
36
|
+
started_at TEXT,
|
|
37
|
+
completed_at TEXT,
|
|
38
|
+
UNIQUE(project_id, idempotency_key)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS run_events (
|
|
42
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
43
|
+
sequence INTEGER NOT NULL,
|
|
44
|
+
type TEXT NOT NULL,
|
|
45
|
+
payload_json TEXT NOT NULL,
|
|
46
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
47
|
+
PRIMARY KEY (run_id, sequence)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_plan_drafts_project
|
|
51
|
+
ON plan_drafts(project_id, updated_at DESC);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_runs_project_status
|
|
53
|
+
ON runs(project_id, status, requested_at DESC);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_run_events_run
|
|
55
|
+
ON run_events(run_id, sequence);
|