Skip to content

Commit 4b246cf

Browse files
committed
fixed the cloud sync issue
1 parent 4fc36dd commit 4b246cf

2 files changed

Lines changed: 96 additions & 104 deletions

File tree

src/commands/run.ts

Lines changed: 62 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -76,62 +76,18 @@ export async function runTests(options: RunOptions = {}) {
7676
const results = await runner.runTests(testCases);
7777
spinner.stop();
7878

79+
// Save to database
7980
// Save to database
8081
const db = new TestDatabase();
8182
db.saveRun(results);
82-
db.close();
83+
84+
// Calculate results for cloud upload (and for sync logic)
85+
const currentRunId = results.id; // Assuming results has ID
8386

8487
// Report results
8588
const reporter = new TestReporter();
8689
reporter.printResults(results, config.outputFormat);
8790

88-
// Calculate results for cloud upload
89-
const testResults = results.results.map((result: TestResult) => {
90-
// Map from internal TestResult to cloud service TestResult
91-
const mappedResult: import('../services/cloud.service').CloudTestResult = {
92-
test_name: result.testCase.description,
93-
test_description: result.testCase.description,
94-
prompt: typeof result.testCase.prompt === 'string'
95-
? result.testCase.prompt
96-
: JSON.stringify(result.testCase.prompt),
97-
input_data: result.testCase.variables,
98-
expected_output: result.expectedOutput,
99-
actual_output: result.actualOutput,
100-
score: result.score,
101-
method: result.testCase.config?.method || 'exact',
102-
status: result.status,
103-
model: result.metadata.provider || '',
104-
tokens_used: result.metadata.tokens,
105-
latency_ms: result.metadata.duration,
106-
cost_usd: result.metadata.cost,
107-
error_message: result.error,
108-
error_type: undefined, // No error type in current TestResult interface
109-
};
110-
return mappedResult;
111-
});
112-
113-
// Calculate total cost from all test results
114-
const totalCost = results.results.reduce((sum, result) => {
115-
return sum + (result.metadata.cost || 0);
116-
}, 0);
117-
118-
const resultsSummary = {
119-
totalTests: results.results.length,
120-
passedTests: results.passed,
121-
failedTests: results.failed,
122-
durationMs: Date.now() - startTime,
123-
totalCost: totalCost || 0.05, // fallback value
124-
tests: testResults,
125-
};
126-
127-
// Print results to console (existing logic)
128-
console.log(chalk.green(`\n✅ ${resultsSummary.passedTests} passed`));
129-
console.log(chalk.red(`❌ ${resultsSummary.failedTests} failed\n`));
130-
131-
// Show upsell hint if tests failed
132-
displayRunSummary(results.results);
133-
134-
// NEW: Cloud upload logic
13591
const isCI = options.ci ||
13692
process.env.CI === 'true' ||
13793
!!process.env.GITHUB_ACTIONS ||
@@ -140,11 +96,13 @@ export async function runTests(options: RunOptions = {}) {
14096
const shouldUpload = options.cloud || isCI;
14197

14298
if (shouldUpload) {
143-
await uploadToCloud(resultsSummary, options);
99+
await syncPendingRuns(db, options);
144100
}
145101

102+
db.close();
103+
146104
// Exit with error code if tests failed
147-
if (resultsSummary.failedTests > 0) {
105+
if (results.failed > 0) {
148106
process.exit(1);
149107
}
150108

@@ -163,86 +121,90 @@ export const runCommand = new Command('run')
163121
await runTests(options);
164122
});
165123

166-
async function uploadToCloud(results: {
167-
totalTests: number;
168-
passedTests: number;
169-
failedTests: number;
170-
durationMs: number;
171-
totalCost: number;
172-
tests: import('../services/cloud.service').CloudTestResult[];
173-
}, options: any) {
124+
async function syncPendingRuns(db: TestDatabase, options: any) {
125+
const pendingRuns = db.getPendingUploads();
126+
127+
if (pendingRuns.length === 0) return;
128+
129+
console.log(chalk.blue(`\n☁️ Syncing ${pendingRuns.length} pending run(s) to Cloud...`));
130+
174131
const cloudService = new CloudService();
175132
await cloudService.init();
176133

177-
const isAuth = await cloudService.isAuthenticated();
178-
179-
if (!isAuth) {
180-
console.log(chalk.yellow('\n⚠️ Not authenticated with Cloud.'));
181-
console.log(chalk.gray('Results saved locally. Run `tuneprompt activate` to enable cloud sync\n'));
134+
if (!(await cloudService.isAuthenticated())) {
135+
console.log(chalk.yellow('⚠️ Not authenticated. Run `tuneprompt activate` first.'));
182136
return;
183137
}
184138

185-
// Get or create project
139+
// Get project ID once
186140
let projectId: string;
187141
try {
188142
const projects = await cloudService.getProjects();
189143
if (projects.length === 0) {
190-
console.log(chalk.blue('📁 Creating default project...'));
191144
const project = await cloudService.createProject('Default Project');
192145
projectId = project.id;
193-
console.log(chalk.green(`✅ Project created: ${projectId}`));
194146
} else {
195-
projectId = projects[0].id; // Use first project
196-
console.log(chalk.gray(`📋 Using existing project: ${projectId}`));
147+
projectId = projects[0].id;
197148
}
198-
} catch (error) {
199-
console.log(chalk.yellow('⚠️ Failed to get project'), error);
149+
} catch (err) {
150+
console.log(chalk.yellow('⚠️ Failed to get project info'));
200151
return;
201152
}
202153

203-
// Get Git context
154+
// Common Git/Env context
204155
let gitContext = {};
205156
try {
206157
gitContext = {
207158
commit_hash: execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim(),
208159
branch_name: execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim(),
209160
commit_message: execSync('git log -1 --pretty=%B', { encoding: 'utf-8' }).trim(),
210161
};
211-
} catch {
212-
// Not a git repo
213-
}
162+
} catch { }
214163

215-
// Detect CI provider
216164
let ciProvider;
217165
if (process.env.GITHUB_ACTIONS) ciProvider = 'github';
218166
else if (process.env.GITLAB_CI) ciProvider = 'gitlab';
219167
else if (process.env.JENKINS_HOME) ciProvider = 'jenkins';
220168
else if (process.env.CIRCLECI) ciProvider = 'circleci';
221169

222-
// Prepare run data
223-
const runData: import('../services/cloud.service').RunData = {
224-
project_id: projectId,
225-
environment: options.ci || process.env.CI ? 'ci' : 'local',
226-
ci_provider: ciProvider,
227-
total_tests: results.totalTests,
228-
passed_tests: results.passedTests,
229-
failed_tests: results.failedTests,
230-
duration_ms: results.durationMs,
231-
cost_usd: results.totalCost,
232-
started_at: new Date(Date.now() - results.durationMs).toISOString(),
233-
completed_at: new Date().toISOString(),
234-
test_results: results.tests,
235-
...gitContext,
236-
};
237-
238-
console.log(chalk.blue('\n☁️ Uploading results to Cloud...'));
239-
240-
const uploadResult = await cloudService.uploadRun(runData);
241-
242-
if (uploadResult.success) {
243-
console.log(chalk.green('✅ Results uploaded successfully'));
244-
console.log(chalk.gray(`View at: ${uploadResult.url}\n`));
245-
} else {
246-
console.log(chalk.yellow('⚠️ Failed to upload results:'), uploadResult.error);
170+
// Upload each run
171+
for (const run of pendingRuns) {
172+
const runData: import('../services/cloud.service').RunData = {
173+
project_id: projectId,
174+
environment: options.ci ? 'ci' : 'local',
175+
ci_provider: ciProvider,
176+
total_tests: run.totalTests,
177+
passed_tests: run.passed,
178+
failed_tests: run.failed,
179+
duration_ms: run.duration,
180+
cost_usd: run.results.reduce((sum, r) => sum + (r.metadata.cost || 0), 0) || 0.05, // fallback
181+
started_at: new Date(run.timestamp.getTime() - run.duration).toISOString(),
182+
completed_at: run.timestamp.toISOString(),
183+
test_results: run.results.map(r => ({
184+
test_name: r.testCase.description,
185+
test_description: r.testCase.description,
186+
prompt: typeof r.testCase.prompt === 'string' ? r.testCase.prompt : JSON.stringify(r.testCase.prompt),
187+
input_data: r.testCase.variables,
188+
expected_output: r.expectedOutput,
189+
actual_output: r.actualOutput,
190+
score: r.score,
191+
method: r.testCase.config?.method || 'exact',
192+
status: r.status,
193+
model: r.metadata.provider || '',
194+
tokens_used: r.metadata.tokens,
195+
latency_ms: r.metadata.duration,
196+
cost_usd: r.metadata.cost,
197+
})),
198+
...gitContext // Applying current git context to old runs slightly inaccurate but acceptable
199+
};
200+
201+
const uploadResult = await cloudService.uploadRun(runData);
202+
203+
if (uploadResult.success) {
204+
db.markAsUploaded(run.id);
205+
console.log(chalk.green(` ✓ Uploaded run from ${run.timestamp.toLocaleTimeString()}`));
206+
} else {
207+
console.log(chalk.red(` ✗ Failed to upload run ${run.id}: ${uploadResult.error}`));
208+
}
247209
}
248210
}

src/storage/database.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,26 @@ export class TestDatabase {
5555
CREATE INDEX IF NOT EXISTS idx_result_run ON test_results(run_id);
5656
`);
5757

58+
// Migration for uploaded status
59+
try {
60+
this.db.exec(`ALTER TABLE test_runs ADD COLUMN uploaded INTEGER DEFAULT 0`);
61+
} catch (e: any) {
62+
// Column might already exist
63+
if (!e.message.includes('duplicate column name')) {
64+
// Ignore safe errors
65+
}
66+
}
67+
5868
// Run external migrations (Phase 2)
59-
// Note: runMigrations is async but contains synchronous better-sqlite3 calls
60-
// so it executes immediately. We catch any promise rejection just in case.
6169
runMigrations(this.db).catch((err: any) => {
6270
console.error('Phase 2 migration failed:', err);
6371
});
6472
}
6573

6674
saveRun(run: TestRun): void {
6775
const insertRun = this.db.prepare(`
68-
INSERT INTO test_runs (id, timestamp, total_tests, passed, failed, duration)
69-
VALUES (?, ?, ?, ?, ?, ?)
76+
INSERT INTO test_runs (id, timestamp, total_tests, passed, failed, duration, uploaded)
77+
VALUES (?, ?, ?, ?, ?, ?, 0)
7078
`);
7179

7280
insertRun.run(
@@ -124,6 +132,28 @@ export class TestDatabase {
124132
}));
125133
}
126134

135+
getPendingUploads(): TestRun[] {
136+
const runs = this.db.prepare(`
137+
SELECT * FROM test_runs
138+
WHERE uploaded = 0 OR uploaded IS NULL
139+
ORDER BY timestamp ASC
140+
`).all() as any[];
141+
142+
return runs.map(run => ({
143+
id: run.id,
144+
timestamp: new Date(run.timestamp),
145+
totalTests: run.total_tests,
146+
passed: run.passed,
147+
failed: run.failed,
148+
duration: run.duration,
149+
results: this.getRunResults(run.id)
150+
}));
151+
}
152+
153+
markAsUploaded(runId: string): void {
154+
this.db.prepare(`UPDATE test_runs SET uploaded = 1 WHERE id = ?`).run(runId);
155+
}
156+
127157
private getRunResults(runId: string): TestResult[] {
128158
const results = this.db.prepare(`
129159
SELECT * FROM test_results WHERE run_id = ?

0 commit comments

Comments
 (0)