@straiffi/archon 1.0.12 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/TestsDialog-CXwEw4cO.js +5 -0
- package/dist/client/assets/badge-DBRMkzQk.js +41 -0
- package/dist/client/assets/index-BRYAyBgT.js +142 -0
- package/dist/client/assets/index-CSG_tXky.css +2 -0
- package/dist/client/assets/rolldown-runtime-BYbx6iT9.js +1 -0
- package/dist/client/index.html +4 -2
- package/dist/server/db.js +2 -0
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +285 -10
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/agent.js +30 -1
- package/dist/server/lib/agent.js.map +1 -1
- package/dist/server/lib/chats.js +7 -1
- package/dist/server/lib/chats.js.map +1 -1
- package/dist/server/lib/projectAutoConfig.js +98 -4
- package/dist/server/lib/projectAutoConfig.js.map +1 -1
- package/dist/server/lib/projects.js +78 -0
- package/dist/server/lib/projects.js.map +1 -1
- package/dist/server/lib/testCommandRunner.js +155 -0
- package/dist/server/lib/testCommandRunner.js.map +1 -0
- package/dist/server/lib/testSessions.js +494 -0
- package/dist/server/lib/testSessions.js.map +1 -0
- package/dist/server/workers/chat.js +1 -1
- package/dist/server/workers/chat.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/index-BhQzJS4j.css +0 -2
- package/dist/client/assets/index-vramqRal.js +0 -176
package/dist/server/index.js
CHANGED
|
@@ -40,6 +40,7 @@ import { JiraPlanningReferenceError, resolveJiraPlanningDescription } from './li
|
|
|
40
40
|
import { getBundlePullRequestRecord, hasOpenBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, upsertBundlePullRequest, } from './lib/bundlePullRequests.js';
|
|
41
41
|
import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listProjects, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, validateRepoPath, } from './lib/projects.js';
|
|
42
42
|
import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runProject, runSetupCommandsInWorkspace, runTicketInWorkspace, stopAllTickets, stopProjectRun, stopTicket } from './lib/run.js';
|
|
43
|
+
import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, findProjectTestCommand, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
|
|
43
44
|
import { getActiveBundleStageChangeHead, undoTicketStage } from './lib/ticketUndo.js';
|
|
44
45
|
import { closeAllTerminalSessions, createProjectTerminalSession, createTerminalSessionForWorkspace, destroyTerminalSessionById, registerTerminalSocketHandlers, } from './lib/terminal.js';
|
|
45
46
|
import { countBundleTickets, createTicketRecord, createBundle, deleteBundle, ensureProjectRootBundle, autoParkTicketIfStale, clearAutoParkDismissalIfNeeded, assignTicketDoneOrder, assignTicketLaneOrder, getBundle, getBundleByName, getBundleByBranch, getBundleRepresentativeTicketContext, isProjectRootBundle, getTicket, isBundledTicket, listTicketsInRunContext, listBundles, listBoardTicketEnrichment, listTickets, resolveTicketBranch, resolveTicketTool, } from './lib/tickets.js';
|
|
@@ -697,6 +698,121 @@ const resolveProjectTargetGitWorkspace = (project) => {
|
|
|
697
698
|
}),
|
|
698
699
|
};
|
|
699
700
|
};
|
|
701
|
+
const stripRepeatedSelfSuffix = (label) => {
|
|
702
|
+
let nextLabel = label.trim();
|
|
703
|
+
while (nextLabel !== '') {
|
|
704
|
+
const match = nextLabel.match(/^(.*?) \(([^()]+)\)$/);
|
|
705
|
+
if (!match) {
|
|
706
|
+
return nextLabel;
|
|
707
|
+
}
|
|
708
|
+
const prefix = match[1]?.trim() ?? '';
|
|
709
|
+
const suffix = match[2]?.trim() ?? '';
|
|
710
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
711
|
+
const normalizedSuffix = suffix.toLowerCase();
|
|
712
|
+
if (normalizedPrefix === normalizedSuffix) {
|
|
713
|
+
nextLabel = prefix;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
if (normalizedPrefix.endsWith(` (${normalizedSuffix})`)) {
|
|
717
|
+
nextLabel = prefix;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
return nextLabel;
|
|
721
|
+
}
|
|
722
|
+
return label.trim();
|
|
723
|
+
};
|
|
724
|
+
const stripRepeatedBranchSuffix = (label, branch) => {
|
|
725
|
+
const trimmedLabel = stripRepeatedSelfSuffix(label);
|
|
726
|
+
const trimmedBranch = branch.trim();
|
|
727
|
+
if (trimmedLabel === '' || trimmedBranch === '') {
|
|
728
|
+
return trimmedLabel;
|
|
729
|
+
}
|
|
730
|
+
const suffix = ` (${trimmedBranch})`;
|
|
731
|
+
let nextLabel = trimmedLabel;
|
|
732
|
+
while (nextLabel.toLowerCase().endsWith(suffix.toLowerCase())) {
|
|
733
|
+
nextLabel = nextLabel.slice(0, -suffix.length).trimEnd();
|
|
734
|
+
}
|
|
735
|
+
return nextLabel || trimmedLabel;
|
|
736
|
+
};
|
|
737
|
+
const getBundleTestTargetLabel = (bundle) => {
|
|
738
|
+
const displayName = stripRepeatedSelfSuffix(bundle.name || bundle.branch || '');
|
|
739
|
+
return bundle.branch ? stripRepeatedBranchSuffix(displayName, bundle.branch) : displayName;
|
|
740
|
+
};
|
|
741
|
+
const resolveExplicitProjectTestWorkspace = (project, targetKind, targetBundleId) => {
|
|
742
|
+
if (targetKind === 'bundle') {
|
|
743
|
+
if (!targetBundleId) {
|
|
744
|
+
return { error: 'target_bundle_id is required for bundle test runs', status: 400 };
|
|
745
|
+
}
|
|
746
|
+
const bundle = getBundle(targetBundleId, project.id);
|
|
747
|
+
if (!bundle || isProjectRootBundle(bundle)) {
|
|
748
|
+
return { error: 'Selected bundle target no longer exists', status: 409 };
|
|
749
|
+
}
|
|
750
|
+
const cwd = resolveExistingWorktreePath(bundle.branch, project);
|
|
751
|
+
if (!cwd) {
|
|
752
|
+
return { error: 'Selected bundle target does not have a prepared worktree', status: 409 };
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
project,
|
|
756
|
+
cwd,
|
|
757
|
+
target_kind: 'bundle',
|
|
758
|
+
target_bundle_id: bundle.id,
|
|
759
|
+
target_label: getBundleTestTargetLabel(bundle),
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
const workspace = resolveProjectRootGitWorkspace(project);
|
|
763
|
+
if ('error' in workspace) {
|
|
764
|
+
return workspace;
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
project,
|
|
768
|
+
cwd: workspace.cwd,
|
|
769
|
+
target_kind: 'repo_root',
|
|
770
|
+
target_bundle_id: null,
|
|
771
|
+
target_label: `Repo root (${workspace.branch})`,
|
|
772
|
+
};
|
|
773
|
+
};
|
|
774
|
+
const resolveTicketTestWorkspace = (ticket) => {
|
|
775
|
+
const workspace = resolveTicketGitWorkspace(ticket);
|
|
776
|
+
if ('error' in workspace) {
|
|
777
|
+
return workspace;
|
|
778
|
+
}
|
|
779
|
+
const project = workspace.project;
|
|
780
|
+
if (ticket.worktree_bundle_id) {
|
|
781
|
+
const bundle = getBundle(ticket.worktree_bundle_id, project.id);
|
|
782
|
+
if (bundle && !isProjectRootBundle(bundle)) {
|
|
783
|
+
return {
|
|
784
|
+
project,
|
|
785
|
+
cwd: workspace.cwd,
|
|
786
|
+
target_kind: 'bundle',
|
|
787
|
+
target_bundle_id: bundle.id,
|
|
788
|
+
target_label: getBundleTestTargetLabel(bundle),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return {
|
|
793
|
+
project,
|
|
794
|
+
cwd: workspace.cwd,
|
|
795
|
+
target_kind: 'repo_root',
|
|
796
|
+
target_bundle_id: null,
|
|
797
|
+
target_label: `Repo root (${workspace.branch})`,
|
|
798
|
+
};
|
|
799
|
+
};
|
|
800
|
+
const parseTestTargetSelection = (value) => {
|
|
801
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || !hasOwn(value, 'kind')) {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
const selection = value;
|
|
805
|
+
if (selection.kind === 'all') {
|
|
806
|
+
return { kind: 'all' };
|
|
807
|
+
}
|
|
808
|
+
if (selection.kind === 'file' && typeof selection.path === 'string') {
|
|
809
|
+
return { kind: 'file', path: selection.path };
|
|
810
|
+
}
|
|
811
|
+
if (selection.kind === 'case' && typeof selection.path === 'string' && typeof selection.name === 'string') {
|
|
812
|
+
return { kind: 'case', path: selection.path, name: selection.name };
|
|
813
|
+
}
|
|
814
|
+
return null;
|
|
815
|
+
};
|
|
700
816
|
const resolveProjectFileSuggestionWorkspace = (project, options = {}) => {
|
|
701
817
|
const ticketId = options.ticketId?.trim() || null;
|
|
702
818
|
if (ticketId) {
|
|
@@ -878,6 +994,16 @@ const bundlePullRequestDiscoveryErrorTimestamps = new Map();
|
|
|
878
994
|
const bundlePullRequestSyncInFlight = new Map();
|
|
879
995
|
const bundlePullRequestSyncErrorTimestamps = new Map();
|
|
880
996
|
const getBundlePullRequestDiscoveryKey = (projectId, bundleId) => `${projectId}:${bundleId}`;
|
|
997
|
+
const startPassiveGitHubWork = (label, callback) => {
|
|
998
|
+
setImmediate(() => {
|
|
999
|
+
try {
|
|
1000
|
+
callback();
|
|
1001
|
+
}
|
|
1002
|
+
catch (error) {
|
|
1003
|
+
console.warn(`[integrations] github passive work failed ${label}`, error);
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
};
|
|
881
1007
|
const isBundlePullRequestDiscoveryCoolingDown = (timestamps, key, cooldownMs) => {
|
|
882
1008
|
const lastAttemptAt = timestamps.get(key);
|
|
883
1009
|
if (lastAttemptAt === undefined) {
|
|
@@ -2102,15 +2228,23 @@ app.get('/tickets/:id', async (req, res) => {
|
|
|
2102
2228
|
if (!matchesTicketProjectContext(ticket, req)) {
|
|
2103
2229
|
return res.status(404).json({ error: 'Not found' });
|
|
2104
2230
|
}
|
|
2231
|
+
let passivePullRequestContext = null;
|
|
2105
2232
|
if (ticket.project_id && ticket.worktree_bundle_id) {
|
|
2106
2233
|
const project = getProjectById(ticket.project_id);
|
|
2107
2234
|
const bundle = project ? getBundle(ticket.worktree_bundle_id, project.id) : null;
|
|
2108
2235
|
if (project && bundle) {
|
|
2236
|
+
passivePullRequestContext = { project, bundle };
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
res.json(ticket);
|
|
2240
|
+
if (passivePullRequestContext) {
|
|
2241
|
+
const { project, bundle } = passivePullRequestContext;
|
|
2242
|
+
startPassiveGitHubWork('ticket-open-pr-sync', () => {
|
|
2109
2243
|
startTrackedBundlePullRequestSyncIfNeeded(project, bundle);
|
|
2110
2244
|
startBundlePullRequestDiscoveryIfNeeded(project, bundle);
|
|
2111
|
-
}
|
|
2245
|
+
});
|
|
2112
2246
|
}
|
|
2113
|
-
return
|
|
2247
|
+
return;
|
|
2114
2248
|
});
|
|
2115
2249
|
app.get('/tickets/:id/review-findings', (req, res) => {
|
|
2116
2250
|
const ticket = getTicketRouteContext(req.params.id);
|
|
@@ -2186,12 +2320,13 @@ app.get('/bundles', async (req, res) => {
|
|
|
2186
2320
|
timing.mark('ensure_project_root_bundle');
|
|
2187
2321
|
const bundles = listBundles(result.project.id);
|
|
2188
2322
|
timing.mark('list_bundles');
|
|
2189
|
-
startProjectBundlePullRequestSyncIfNeeded(result.project, bundles);
|
|
2190
|
-
timing.mark('start_pr_sync');
|
|
2191
2323
|
const serializedBundles = bundles.map(bundle => serializeBundleRow(result.project.id, bundle));
|
|
2192
2324
|
timing.mark('serialize_bundles');
|
|
2193
2325
|
timing.end({ count: serializedBundles.length });
|
|
2194
2326
|
res.json(serializedBundles);
|
|
2327
|
+
startPassiveGitHubWork('project-bundle-pr-sync', () => {
|
|
2328
|
+
startProjectBundlePullRequestSyncIfNeeded(result.project, bundles);
|
|
2329
|
+
});
|
|
2195
2330
|
});
|
|
2196
2331
|
app.get('/bundles/:id/details', async (req, res) => {
|
|
2197
2332
|
const result = getRequiredProject(req);
|
|
@@ -2215,9 +2350,11 @@ app.get('/bundles/:id/conversation', (req, res) => {
|
|
|
2215
2350
|
if (!bundle) {
|
|
2216
2351
|
return res.status(404).json({ error: 'Bundle not found' });
|
|
2217
2352
|
}
|
|
2218
|
-
startBundlePullRequestDiscoveryIfNeeded(result.project, bundle);
|
|
2219
2353
|
const payload = serializeBundleConversationDetails(result.project, bundle);
|
|
2220
2354
|
res.json(payload);
|
|
2355
|
+
startPassiveGitHubWork('bundle-conversation-pr-discovery', () => {
|
|
2356
|
+
startBundlePullRequestDiscoveryIfNeeded(result.project, bundle);
|
|
2357
|
+
});
|
|
2221
2358
|
});
|
|
2222
2359
|
app.get('/bundles/:id/review-findings', (req, res) => {
|
|
2223
2360
|
const result = getRequiredProject(req);
|
|
@@ -3203,7 +3340,7 @@ app.post('/projects', (req, res) => {
|
|
|
3203
3340
|
return res.status(400).json({ error: validation.error });
|
|
3204
3341
|
}
|
|
3205
3342
|
const projectCount = Number(db.prepare('SELECT COUNT(*) AS count FROM projects').get()?.count ?? 0);
|
|
3206
|
-
const { name, repo_path, linked_project_ids, run_setup, run_services, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, auto_park_stale_tickets, memory_enabled, worktree_sync, } = validation.values;
|
|
3343
|
+
const { name, repo_path, linked_project_ids, run_setup, run_services, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, auto_park_stale_tickets, memory_enabled, worktree_sync, } = validation.values;
|
|
3207
3344
|
const id = randomUUID();
|
|
3208
3345
|
db.prepare(`
|
|
3209
3346
|
INSERT INTO projects (
|
|
@@ -3212,6 +3349,7 @@ app.post('/projects', (req, res) => {
|
|
|
3212
3349
|
repo_path,
|
|
3213
3350
|
run_setup,
|
|
3214
3351
|
run_services,
|
|
3352
|
+
test_commands,
|
|
3215
3353
|
run_ide,
|
|
3216
3354
|
preview_service_name,
|
|
3217
3355
|
preview_path,
|
|
@@ -3223,8 +3361,8 @@ app.post('/projects', (req, res) => {
|
|
|
3223
3361
|
memory_enabled,
|
|
3224
3362
|
worktree_sync
|
|
3225
3363
|
)
|
|
3226
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3227
|
-
`).run(id, name, repo_path, run_setup, run_services, run_ide ?? null, preview_service_name ?? null, preview_path ?? null, preview_capability_mode ?? null, helper_model ?? null, helper_variant ?? null, commit_message_rules ?? null, auto_park_stale_tickets ?? 0, memory_enabled ?? 0, worktree_sync ?? null);
|
|
3364
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3365
|
+
`).run(id, name, repo_path, run_setup, run_services, test_commands, run_ide ?? null, preview_service_name ?? null, preview_path ?? null, preview_capability_mode ?? null, helper_model ?? null, helper_variant ?? null, commit_message_rules ?? null, auto_park_stale_tickets ?? 0, memory_enabled ?? 0, worktree_sync ?? null);
|
|
3228
3366
|
replaceProjectLinks(id, linked_project_ids ?? []);
|
|
3229
3367
|
ensureProjectRootBundle(id);
|
|
3230
3368
|
if (projectCount === 0) {
|
|
@@ -4101,13 +4239,14 @@ app.patch('/projects/:id', (req, res) => {
|
|
|
4101
4239
|
});
|
|
4102
4240
|
}
|
|
4103
4241
|
}
|
|
4104
|
-
const { name, repo_path, linked_project_ids, run_setup, run_services, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, auto_park_stale_tickets, memory_enabled, worktree_sync, } = validation.values;
|
|
4242
|
+
const { name, repo_path, linked_project_ids, run_setup, run_services, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, auto_park_stale_tickets, memory_enabled, worktree_sync, } = validation.values;
|
|
4105
4243
|
db.prepare(`
|
|
4106
4244
|
UPDATE projects SET
|
|
4107
4245
|
name = ?,
|
|
4108
4246
|
repo_path = ?,
|
|
4109
4247
|
run_setup = ?,
|
|
4110
4248
|
run_services = ?,
|
|
4249
|
+
test_commands = ?,
|
|
4111
4250
|
run_ide = ?,
|
|
4112
4251
|
preview_service_name = ?,
|
|
4113
4252
|
preview_path = ?,
|
|
@@ -4120,7 +4259,7 @@ app.patch('/projects/:id', (req, res) => {
|
|
|
4120
4259
|
worktree_sync = ?,
|
|
4121
4260
|
updated_at = CURRENT_TIMESTAMP
|
|
4122
4261
|
WHERE id = ?
|
|
4123
|
-
`).run(name ?? project.name, repo_path ?? project.repo_path, hasOwn(validation.values, 'run_setup') ? run_setup : project.run_setup, hasOwn(validation.values, 'run_services') ? run_services : project.run_services, hasOwn(validation.values, 'run_ide') ? run_ide : project.run_ide, hasOwn(validation.values, 'preview_service_name') ? preview_service_name : project.preview_service_name, hasOwn(validation.values, 'preview_path') ? preview_path : project.preview_path, hasOwn(validation.values, 'preview_capability_mode') ? preview_capability_mode : project.preview_capability_mode, hasOwn(validation.values, 'helper_model') ? helper_model : project.helper_model, hasOwn(validation.values, 'helper_variant') ? helper_variant : project.helper_variant, hasOwn(validation.values, 'commit_message_rules') ? commit_message_rules : project.commit_message_rules, hasOwn(validation.values, 'auto_park_stale_tickets') ? auto_park_stale_tickets : project.auto_park_stale_tickets, hasOwn(validation.values, 'memory_enabled') ? memory_enabled : project.memory_enabled, hasOwn(validation.values, 'worktree_sync') ? worktree_sync : project.worktree_sync, req.params.id);
|
|
4262
|
+
`).run(name ?? project.name, repo_path ?? project.repo_path, hasOwn(validation.values, 'run_setup') ? run_setup : project.run_setup, hasOwn(validation.values, 'run_services') ? run_services : project.run_services, hasOwn(validation.values, 'test_commands') ? test_commands : project.test_commands, hasOwn(validation.values, 'run_ide') ? run_ide : project.run_ide, hasOwn(validation.values, 'preview_service_name') ? preview_service_name : project.preview_service_name, hasOwn(validation.values, 'preview_path') ? preview_path : project.preview_path, hasOwn(validation.values, 'preview_capability_mode') ? preview_capability_mode : project.preview_capability_mode, hasOwn(validation.values, 'helper_model') ? helper_model : project.helper_model, hasOwn(validation.values, 'helper_variant') ? helper_variant : project.helper_variant, hasOwn(validation.values, 'commit_message_rules') ? commit_message_rules : project.commit_message_rules, hasOwn(validation.values, 'auto_park_stale_tickets') ? auto_park_stale_tickets : project.auto_park_stale_tickets, hasOwn(validation.values, 'memory_enabled') ? memory_enabled : project.memory_enabled, hasOwn(validation.values, 'worktree_sync') ? worktree_sync : project.worktree_sync, req.params.id);
|
|
4124
4263
|
if (hasOwn(validation.values, 'linked_project_ids')) {
|
|
4125
4264
|
replaceProjectLinks(project.id, linked_project_ids ?? []);
|
|
4126
4265
|
}
|
|
@@ -4881,6 +5020,141 @@ app.post('/projects/:id/run', (req, res) => {
|
|
|
4881
5020
|
targetBundleId: workspace.target_bundle_id,
|
|
4882
5021
|
});
|
|
4883
5022
|
});
|
|
5023
|
+
app.post('/projects/:id/test-sessions', (req, res) => {
|
|
5024
|
+
const project = getProjectById(req.params.id);
|
|
5025
|
+
if (!project) {
|
|
5026
|
+
return res.status(404).json({ error: 'Not found' });
|
|
5027
|
+
}
|
|
5028
|
+
const body = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? req.body : null;
|
|
5029
|
+
const targetKind = body?.target_kind === 'bundle'
|
|
5030
|
+
? 'bundle'
|
|
5031
|
+
: body?.target_kind === 'repo_root'
|
|
5032
|
+
? 'repo_root'
|
|
5033
|
+
: null;
|
|
5034
|
+
if (!targetKind) {
|
|
5035
|
+
return res.status(400).json({ error: 'target_kind must be repo_root or bundle' });
|
|
5036
|
+
}
|
|
5037
|
+
const targetBundleId = typeof body?.target_bundle_id === 'string' ? body.target_bundle_id : null;
|
|
5038
|
+
const workspace = resolveExplicitProjectTestWorkspace(project, targetKind, targetBundleId);
|
|
5039
|
+
if ('error' in workspace) {
|
|
5040
|
+
return res.status(Number(workspace.status)).json({ error: workspace.error });
|
|
5041
|
+
}
|
|
5042
|
+
return res.status(201).json(createTestSession({
|
|
5043
|
+
projectId: project.id,
|
|
5044
|
+
targetKind: workspace.target_kind,
|
|
5045
|
+
targetBundleId: workspace.target_bundle_id,
|
|
5046
|
+
cwd: workspace.cwd,
|
|
5047
|
+
targetLabel: workspace.target_label,
|
|
5048
|
+
}));
|
|
5049
|
+
});
|
|
5050
|
+
app.post('/tickets/:id/test-sessions', (req, res) => {
|
|
5051
|
+
const ticket = getTicket(req.params.id);
|
|
5052
|
+
if (!ticket) {
|
|
5053
|
+
return res.status(404).json({ error: 'Not found' });
|
|
5054
|
+
}
|
|
5055
|
+
if (!matchesTicketProjectContext(ticket, req)) {
|
|
5056
|
+
return res.status(404).json({ error: 'Not found' });
|
|
5057
|
+
}
|
|
5058
|
+
const workspace = resolveTicketTestWorkspace(ticket);
|
|
5059
|
+
if ('error' in workspace) {
|
|
5060
|
+
return res.status(workspace.status).json({ error: workspace.error });
|
|
5061
|
+
}
|
|
5062
|
+
return res.status(201).json(createTestSession({
|
|
5063
|
+
projectId: workspace.project.id,
|
|
5064
|
+
ticketId: ticket.id,
|
|
5065
|
+
targetKind: workspace.target_kind,
|
|
5066
|
+
targetBundleId: workspace.target_bundle_id,
|
|
5067
|
+
cwd: workspace.cwd,
|
|
5068
|
+
targetLabel: workspace.target_label,
|
|
5069
|
+
}));
|
|
5070
|
+
});
|
|
5071
|
+
app.get('/test-sessions/:id/discovery', (req, res) => {
|
|
5072
|
+
const session = getTestSession(req.params.id);
|
|
5073
|
+
if (!session) {
|
|
5074
|
+
return res.status(404).json({ error: 'Not found' });
|
|
5075
|
+
}
|
|
5076
|
+
const commandId = typeof req.query.command_id === 'string' ? req.query.command_id.trim() : '';
|
|
5077
|
+
let command = null;
|
|
5078
|
+
if (commandId !== '') {
|
|
5079
|
+
const project = getProjectById(session.project_id);
|
|
5080
|
+
if (!project) {
|
|
5081
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
5082
|
+
}
|
|
5083
|
+
command = findProjectTestCommand(project, commandId);
|
|
5084
|
+
if (!command) {
|
|
5085
|
+
return res.status(404).json({ error: 'Test command not found' });
|
|
5086
|
+
}
|
|
5087
|
+
}
|
|
5088
|
+
try {
|
|
5089
|
+
const supportsGranularSelection = command
|
|
5090
|
+
? isTestSelectionSupported(command, { kind: 'file', path: '' }, session.cwd)
|
|
5091
|
+
: false;
|
|
5092
|
+
return res.json({
|
|
5093
|
+
files: discoverTestsForSession(session.id, command),
|
|
5094
|
+
supports_granular_selection: supportsGranularSelection,
|
|
5095
|
+
});
|
|
5096
|
+
}
|
|
5097
|
+
catch (error) {
|
|
5098
|
+
return res.status(500).json({ error: getErrorMessage(error) });
|
|
5099
|
+
}
|
|
5100
|
+
});
|
|
5101
|
+
app.post('/test-sessions/:id/run', async (req, res) => {
|
|
5102
|
+
const session = getTestSession(req.params.id);
|
|
5103
|
+
if (!session) {
|
|
5104
|
+
return res.status(404).json({ error: 'Not found' });
|
|
5105
|
+
}
|
|
5106
|
+
const project = getProjectById(session.project_id);
|
|
5107
|
+
if (!project) {
|
|
5108
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
5109
|
+
}
|
|
5110
|
+
const body = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? req.body : null;
|
|
5111
|
+
const commandId = typeof body?.command_id === 'string' ? body.command_id.trim() : '';
|
|
5112
|
+
if (commandId === '') {
|
|
5113
|
+
return res.status(400).json({ error: 'command_id is required' });
|
|
5114
|
+
}
|
|
5115
|
+
const selection = parseTestTargetSelection(body?.selection);
|
|
5116
|
+
if (!selection) {
|
|
5117
|
+
return res.status(400).json({ error: 'selection must be all, file, or case' });
|
|
5118
|
+
}
|
|
5119
|
+
const command = findProjectTestCommand(project, commandId);
|
|
5120
|
+
if (!command) {
|
|
5121
|
+
return res.status(404).json({ error: 'Test command not found' });
|
|
5122
|
+
}
|
|
5123
|
+
if (!isTestSelectionSupported(command, selection, session.cwd)) {
|
|
5124
|
+
return res.status(409).json({ error: 'The selected test command only supports running the full suite.' });
|
|
5125
|
+
}
|
|
5126
|
+
if (selection.kind !== 'all' && !doesTestFileExistForSession(session.id, selection.path, command)) {
|
|
5127
|
+
return res.status(404).json({ error: 'Selected test file no longer exists in this target workspace.' });
|
|
5128
|
+
}
|
|
5129
|
+
try {
|
|
5130
|
+
return res.json(await startTestSessionRun(session.id, project, {
|
|
5131
|
+
command,
|
|
5132
|
+
selection,
|
|
5133
|
+
}, io));
|
|
5134
|
+
}
|
|
5135
|
+
catch (error) {
|
|
5136
|
+
return res.status(409).json({ error: getErrorMessage(error) });
|
|
5137
|
+
}
|
|
5138
|
+
});
|
|
5139
|
+
app.post('/test-sessions/:id/stop', async (req, res) => {
|
|
5140
|
+
const session = getTestSession(req.params.id);
|
|
5141
|
+
if (!session) {
|
|
5142
|
+
return res.status(404).json({ error: 'Not found' });
|
|
5143
|
+
}
|
|
5144
|
+
try {
|
|
5145
|
+
return res.json(await stopTestSessionRun(session.id, io));
|
|
5146
|
+
}
|
|
5147
|
+
catch (error) {
|
|
5148
|
+
return res.status(500).json({ error: getErrorMessage(error) });
|
|
5149
|
+
}
|
|
5150
|
+
});
|
|
5151
|
+
app.delete('/test-sessions/:id', async (req, res) => {
|
|
5152
|
+
const deleted = await deleteTestSession(req.params.id, io);
|
|
5153
|
+
if (!deleted) {
|
|
5154
|
+
return res.status(404).json({ error: 'Not found' });
|
|
5155
|
+
}
|
|
5156
|
+
return res.status(204).send();
|
|
5157
|
+
});
|
|
4884
5158
|
app.post('/tickets/:id/terminal', (req, res) => {
|
|
4885
5159
|
const ticket = getTicket(req.params.id);
|
|
4886
5160
|
if (!ticket) {
|
|
@@ -5198,6 +5472,7 @@ export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
|
|
|
5198
5472
|
});
|
|
5199
5473
|
};
|
|
5200
5474
|
export const shutdownServer = async (signal) => {
|
|
5475
|
+
await clearAllTestSessions(io);
|
|
5201
5476
|
await shutdownRealtimeServer({
|
|
5202
5477
|
signal,
|
|
5203
5478
|
io,
|