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
5 changes: 5 additions & 0 deletions .changeset/fix-dev-session-logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@agent-native/core": patch
---

Add [dev-session] log when auto-binding email in CLI runner; fix TS narrowing in db-reset-dev-owner; remove redundant trim in zeroChangesHint.
31 changes: 30 additions & 1 deletion packages/core/src/scripts/db/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,32 @@ function printResult(
if (result.lastInsertRowid && changes > 0) {
console.log(`Last Insert Row ID: ${result.lastInsertRowid}`);
}
if (changes === 0) {
console.log(zeroChangesHint(sql));
}
}
}

/**
* Hint emitted when an UPDATE/DELETE/REPLACE matches zero rows. Matches the
* wording used by db-patch's "no rows matched" error so the agent gets the
* same scoping nudge from both tools — without this hint, the agent reports
* "Changes: 0" as success and the user sees no UI update because the row
* either didn't exist or wasn't visible to the current user under per-user
* scoping.
*/
function zeroChangesHint(sql: string): string {
const upper = sql.toUpperCase(); // leading whitespace already stripped by normalizeUserSql
if (upper.startsWith("INSERT")) {
// INSERT changes=0 means INSERT OR IGNORE skipped a duplicate — different
// failure mode, not a scoping issue.
return "Hint: 0 rows inserted. The row likely violated a UNIQUE / PRIMARY KEY constraint and was skipped (INSERT OR IGNORE).";
}
return (
"Hint: 0 rows changed. The WHERE clause matched no rows — either the row " +
"doesn't exist, or it exists but is owned by a different user (per-user " +
"and per-org scoping is automatic for db-exec)."
);
}

function printBatchResult(results: DbExecResult[], format?: string): void {
Expand Down Expand Up @@ -542,7 +567,11 @@ function printBatchResult(results: DbExecResult[], format?: string): void {
console.log(`[${result.index}] Returned ${result.rows.length} row(s):`);
console.log(JSON.stringify(result.rows, null, 2));
} else {
console.log(`[${result.index}] Changes: ${result.changes ?? 0}`);
const changes = Number(result.changes ?? 0);
console.log(`[${result.index}] Changes: ${changes}`);
if (changes === 0) {
console.log(`[${result.index}] ${zeroChangesHint(result.sql)}`);
}
}
}
console.log(`Total changes: ${totalChanges}`);
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/scripts/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ export const coreDbScripts: Record<string, (args: string[]) => Promise<void>> =
import("./wipe-leaked-builder-keys.js").then((m) => m.default(args)),
"db-migrate-user-api-keys": (args) =>
import("./migrate-user-api-keys.js").then((m) => m.default(args)),
"db-reset-dev-owner": (args) =>
import("./reset-dev-owner.js").then((m) => m.default(args)),
};
34 changes: 24 additions & 10 deletions packages/core/src/scripts/db/parameterized.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe("db scripts parameterized SQL", () => {
}

it("passes db-query bind args through to libsql", async () => {
vi.stubEnv("AGENT_USER_EMAIL", "params+qa@test.com");
const execute = vi.fn(async (input: unknown) => {
if (typeof input === "object" && input) {
return { rows: [["ada"]], columns: ["name"] };
Expand All @@ -71,6 +72,7 @@ describe("db scripts parameterized SQL", () => {
});

it("passes db-exec bind args through to libsql", async () => {
vi.stubEnv("AGENT_USER_EMAIL", "params+qa@test.com");
const execute = vi.fn(async () => ({
rows: [],
columns: [],
Expand All @@ -97,12 +99,21 @@ describe("db scripts parameterized SQL", () => {
});

it("executes db-exec statement batches in one SQLite transaction", async () => {
const execute = vi.fn(async () => ({
rows: [],
columns: [],
rowsAffected: 1,
lastInsertRowid: undefined,
}));
vi.stubEnv("AGENT_USER_EMAIL", "params+qa@test.com");
// Return an empty sqlite_master so scoping introspection doesn't generate
// setup views — keeps this test focused on the BEGIN/INSERT/UPDATE/COMMIT
// ordering. The first call is the introspection SELECT that returns [].
const execute = vi.fn(async (input: unknown) => {
if (typeof input === "string" && input.includes("sqlite_master")) {
return { rows: [], columns: [] };
}
return {
rows: [],
columns: [],
rowsAffected: 1,
lastInsertRowid: undefined,
};
});
mockSqliteClient(execute);

const { default: dbExec } = await import("./exec.js");
Expand All @@ -123,16 +134,19 @@ describe("db scripts parameterized SQL", () => {
"json",
]);

expect(execute).toHaveBeenNthCalledWith(1, "BEGIN");
expect(execute).toHaveBeenNthCalledWith(2, {
const txCalls = execute.mock.calls.filter(
([arg]) => !(typeof arg === "string" && arg.includes("sqlite_master")),
);
expect(txCalls[0]?.[0]).toBe("BEGIN");
expect(txCalls[1]?.[0]).toEqual({
sql: "INSERT INTO notes (id, title) VALUES (?, ?)",
args: ["note-1", "One"],
});
expect(execute).toHaveBeenNthCalledWith(3, {
expect(txCalls[2]?.[0]).toEqual({
sql: "UPDATE notes SET title = ? WHERE id = ?",
args: ["Two", "note-1"],
});
expect(execute).toHaveBeenNthCalledWith(4, "COMMIT");
expect(txCalls[3]?.[0]).toBe("COMMIT");
});

it("rejects ad-hoc schema changes through db-exec", async () => {
Expand Down
Loading
Loading