Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/mobile-showcase-screenshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ jobs:
args:
- --filter=@t3tools/mobile...
- --filter=@t3tools/scripts...
- --filter=t3...
- name: Expose pnpm
run: |
Expand Down Expand Up @@ -100,6 +101,7 @@ jobs:
args:
- --filter=@t3tools/mobile...
- --filter=@t3tools/scripts...
- --filter=t3...
- name: Expose pnpm
run: |
Expand Down
59 changes: 47 additions & 12 deletions scripts/mobile-showcase-environment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @effect-diagnostics nodeBuiltinImport:off globalDate:off - This host-side fixture creates an isolated local T3 environment.
// @effect-diagnostics nodeBuiltinImport:off globalTimers:off globalDate:off - This host-side fixture creates an isolated local T3 environment.
import * as NodeChildProcess from "node:child_process";
import * as NodeFSP from "node:fs/promises";
import * as NodePath from "node:path";
Expand Down Expand Up @@ -387,6 +387,48 @@ function insertThread(
.run(input.id, isWorking ? "running" : "ready", isWorking ? turnId : null, updatedAt);
}

const SEEDED_PROJECTION_TABLES = [
"projection_pending_approvals",
"projection_thread_proposed_plans",
"projection_thread_activities",
"projection_thread_messages",
"projection_thread_sessions",
"projection_turns",
"projection_threads",
"projection_projects",
"projection_state",
] as const;

function hasSeedableSchema(dbPath: string): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High scripts/mobile-showcase-environment.ts:402

hasSeedableSchema returns true as soon as all nine table names exist in sqlite_master, but it never checks whether the columns that seedDatabase inserts into are present. Columns like projection_threads.runtime_mode, interaction_mode, and model_selection_json are added by later migrations, so if polling observes the database after the tables are created but before those migrations finish, hasSeedableSchema returns true and seedDatabase then fails with a missing-column error. The readiness check should verify the completed migration version or the specific columns required by the seed inserts, not just table existence.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @scripts/mobile-showcase-environment.ts around line 402:

`hasSeedableSchema` returns `true` as soon as all nine table names exist in `sqlite_master`, but it never checks whether the columns that `seedDatabase` inserts into are present. Columns like `projection_threads.runtime_mode`, `interaction_mode`, and `model_selection_json` are added by later migrations, so if polling observes the database after the tables are created but before those migrations finish, `hasSeedableSchema` returns `true` and `seedDatabase` then fails with a missing-column error. The readiness check should verify the completed migration version or the specific columns required by the seed inserts, not just table existence.

let database: NodeSqlite.DatabaseSync;
try {
database = new NodeSqlite.DatabaseSync(dbPath, { readOnly: true });
} catch {
return false;
}
try {
const row = database
.prepare(
`SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name IN (${SEEDED_PROJECTION_TABLES.map(() => "?").join(", ")})`,
)
.get(...SEEDED_PROJECTION_TABLES) as { count: number };
return row.count === SEEDED_PROJECTION_TABLES.length;
} catch {
return false;
} finally {
database.close();
}
}

async function waitForSeedableSchema(dbPath: string, timeoutMs = 60_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (hasSeedableSchema(dbPath)) return;
await new Promise((resolve) => setTimeout(resolve, 250));
}
throw new Error(`The environment server did not migrate ${dbPath} within ${timeoutMs}ms.`);
}

function seedDatabase(
dbPath: string,
workspaceRoots: ReadonlyMap<string, string>,
Expand All @@ -401,17 +443,7 @@ function seedDatabase(
const database = new NodeSqlite.DatabaseSync(dbPath, { timeout: 30_000 });
try {
database.exec("BEGIN IMMEDIATE");
for (const table of [
"projection_pending_approvals",
"projection_thread_proposed_plans",
"projection_thread_activities",
"projection_thread_messages",
"projection_thread_sessions",
"projection_turns",
"projection_threads",
"projection_projects",
"projection_state",
]) {
for (const table of SEEDED_PROJECTION_TABLES) {
database.exec(`DELETE FROM ${table}`);
}
const insertProject = database.prepare(
Expand Down Expand Up @@ -594,6 +626,9 @@ export async function seedShowcaseEnvironment(input: {
});
}),
);
// The environment server begins listening before it finishes migrating the
// database, so wait for the schema before deleting from and reseeding it.
await waitForSeedableSchema(dbPath);
seedDatabase(dbPath, workspaceRoots, projects, threads, now);

const terminalDirectory = NodePath.join(input.baseDir, "userdata", "logs", "terminals");
Expand Down
29 changes: 23 additions & 6 deletions scripts/mobile-showcase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,23 @@ async function waitForPort(port: number, label = "Process", timeoutMs = 60_000):
throw new Error(`${label} did not begin listening on port ${port} within ${timeoutMs}ms.`);
}

async function waitForFileContent(
filePath: string,
label: string,
timeoutMs = 60_000,
): Promise<string> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const content = await NodeFSP.readFile(filePath, "utf8").then(
(value) => value.trim(),
() => "",
);
if (content) return content;
await delay(250);
}
throw new Error(`${label} was not written to ${filePath} within ${timeoutMs}ms.`);
}

async function reserveAvailablePort(): Promise<number> {
return await new Promise<number>((resolve, reject) => {
const server = NodeNet.createServer();
Expand Down Expand Up @@ -1225,12 +1242,12 @@ async function main(): Promise<void> {
showcaseServers.push(server);
await waitForPort(port, `${environment.label} server`);
await seedShowcaseEnvironment({ baseDir, projectIds: environment.projectIds });
const environmentId = (
await NodeFSP.readFile(NodePath.join(baseDir, "userdata", "environment-id"), "utf8")
).trim();
if (!environmentId) {
throw new Error(`${environment.label} did not persist an environment id.`);
}
// The server begins listening before the ServerEnvironment layer
// persists the environment id, so poll rather than read once.
const environmentId = await waitForFileContent(
NodePath.join(baseDir, "userdata", "environment-id"),
`${environment.label} environment id`,
);
showcaseEnvironments.push({ baseDir, environmentId, label: environment.label, port });
}

Expand Down
Loading