chore: generate
This commit is contained in:
parent
7f571d36ea
commit
102c8353e0
71 changed files with 12548 additions and 10185 deletions
|
|
@ -2,10 +2,7 @@
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "bf93c73b-5a48-4d63-9909-3c36a79b9788",
|
"id": "bf93c73b-5a48-4d63-9909-3c36a79b9788",
|
||||||
"prevIds": [
|
"prevIds": ["be5eae31-b7f8-4292-8827-c36a524abd1b", "fdfcccee-fb3a-481f-b801-b9835fa30d5d"],
|
||||||
"be5eae31-b7f8-4292-8827-c36a524abd1b",
|
|
||||||
"fdfcccee-fb3a-481f-b801-b9835fa30d5d"
|
|
||||||
],
|
|
||||||
"ddl": [
|
"ddl": [
|
||||||
{
|
{
|
||||||
"name": "workspace",
|
"name": "workspace",
|
||||||
|
|
@ -1188,13 +1185,9 @@
|
||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1203,13 +1196,9 @@
|
||||||
"table": "workspace"
|
"table": "workspace"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["active_account_id"],
|
||||||
"active_account_id"
|
|
||||||
],
|
|
||||||
"tableTo": "account",
|
"tableTo": "account",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "SET NULL",
|
"onDelete": "SET NULL",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1218,13 +1207,9 @@
|
||||||
"table": "account_state"
|
"table": "account_state"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["aggregate_id"],
|
||||||
"aggregate_id"
|
|
||||||
],
|
|
||||||
"tableTo": "event_sequence",
|
"tableTo": "event_sequence",
|
||||||
"columnsTo": [
|
"columnsTo": ["aggregate_id"],
|
||||||
"aggregate_id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1233,13 +1218,9 @@
|
||||||
"table": "event"
|
"table": "event"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1248,13 +1229,9 @@
|
||||||
"table": "message"
|
"table": "message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["message_id"],
|
||||||
"message_id"
|
|
||||||
],
|
|
||||||
"tableTo": "message",
|
"tableTo": "message",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1263,13 +1240,9 @@
|
||||||
"table": "part"
|
"table": "part"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1278,13 +1251,9 @@
|
||||||
"table": "permission"
|
"table": "permission"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1293,13 +1262,9 @@
|
||||||
"table": "session_message"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1308,13 +1273,9 @@
|
||||||
"table": "session"
|
"table": "session"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1323,13 +1284,9 @@
|
||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1338,137 +1295,105 @@
|
||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["email", "url"],
|
||||||
"email",
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "control_account_pk",
|
"name": "control_account_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "control_account"
|
"table": "control_account"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id", "position"],
|
||||||
"session_id",
|
|
||||||
"position"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "todo_pk",
|
"name": "todo_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "workspace_pk",
|
"name": "workspace_pk",
|
||||||
"table": "workspace",
|
"table": "workspace",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["name"],
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "data_migration_pk",
|
"name": "data_migration_pk",
|
||||||
"table": "data_migration",
|
"table": "data_migration",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_state_pk",
|
"name": "account_state_pk",
|
||||||
"table": "account_state",
|
"table": "account_state",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_pk",
|
"name": "account_pk",
|
||||||
"table": "account",
|
"table": "account",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["aggregate_id"],
|
||||||
"aggregate_id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_sequence_pk",
|
"name": "event_sequence_pk",
|
||||||
"table": "event_sequence",
|
"table": "event_sequence",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_pk",
|
"name": "event_pk",
|
||||||
"table": "event",
|
"table": "event",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "project_pk",
|
"name": "project_pk",
|
||||||
"table": "project",
|
"table": "project",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "message_pk",
|
"name": "message_pk",
|
||||||
"table": "message",
|
"table": "message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "part_pk",
|
"name": "part_pk",
|
||||||
"table": "part",
|
"table": "part",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "permission_pk",
|
"name": "permission_pk",
|
||||||
"table": "permission",
|
"table": "permission",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_message_pk",
|
"name": "session_message_pk",
|
||||||
"table": "session_message",
|
"table": "session_message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_pk",
|
"name": "session_pk",
|
||||||
"table": "session",
|
"table": "session",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_share_pk",
|
"name": "session_share_pk",
|
||||||
"table": "session_share",
|
"table": "session_share",
|
||||||
|
|
@ -1632,4 +1557,4 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"renames": []
|
"renames": []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,10 @@ const sqlMigrations = (await Array.fromAsync(new Bun.Glob("*/migration.sql").sca
|
||||||
|
|
||||||
for (const name of sqlMigrations) {
|
for (const name of sqlMigrations) {
|
||||||
if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
|
if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
|
||||||
await Bun.write(path.join(tsDir, `${name}.ts`), renderMigration(name, await Bun.file(path.join(sqlDir, name, "migration.sql")).text()))
|
await Bun.write(
|
||||||
|
path.join(tsDir, `${name}.ts`),
|
||||||
|
renderMigration(name, await Bun.file(path.join(sqlDir, name, "migration.sql")).text()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await Bun.write(registry, renderRegistry(sqlMigrations))
|
await Bun.write(registry, renderRegistry(sqlMigrations))
|
||||||
|
|
@ -47,7 +50,9 @@ export default { ...config, out: ${JSON.stringify(output)} }
|
||||||
await $`bun drizzle-kit generate --config ${config}`.cwd(path.join(root, "packages/core"))
|
await $`bun drizzle-kit generate --config ${config}`.cwd(path.join(root, "packages/core"))
|
||||||
const after = await snapshot(output)
|
const after = await snapshot(output)
|
||||||
if (JSON.stringify(after) !== JSON.stringify(before)) {
|
if (JSON.stringify(after) !== JSON.stringify(before)) {
|
||||||
throw new Error("Core schema has ungenerated database migrations. Run `bun script/migration.ts` from packages/core.")
|
throw new Error(
|
||||||
|
"Core schema has ungenerated database migrations. Run `bun script/migration.ts` from packages/core.",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrations = before
|
const migrations = before
|
||||||
|
|
@ -56,7 +61,9 @@ export default { ...config, out: ${JSON.stringify(output)} }
|
||||||
.sort()
|
.sort()
|
||||||
for (const name of migrations) {
|
for (const name of migrations) {
|
||||||
if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
|
if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
|
||||||
throw new Error(`Database migration TypeScript wrapper is missing for ${name}. Run \`bun script/migration.ts\` from packages/core.`)
|
throw new Error(
|
||||||
|
`Database migration TypeScript wrapper is missing for ${name}. Run \`bun script/migration.ts\` from packages/core.`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if ((await Bun.file(registry).text()) !== renderRegistry(migrations)) {
|
if ((await Bun.file(registry).text()) !== renderRegistry(migrations)) {
|
||||||
throw new Error("Database migration registry is stale. Run `bun script/migration.ts` from packages/core.")
|
throw new Error("Database migration registry is stale. Run `bun script/migration.ts` from packages/core.")
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,4 @@ export const layer = Layer.effect(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(Layer.provide(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))))
|
||||||
Layer.provide(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,27 @@
|
||||||
import type { DatabaseMigration } from "./migration"
|
import type { DatabaseMigration } from "./migration"
|
||||||
|
|
||||||
export const migrations = (await Promise.all([
|
export const migrations = (
|
||||||
import("./migration/20260127222353_familiar_lady_ursula"),
|
await Promise.all([
|
||||||
import("./migration/20260211171708_add_project_commands"),
|
import("./migration/20260127222353_familiar_lady_ursula"),
|
||||||
import("./migration/20260213144116_wakeful_the_professor"),
|
import("./migration/20260211171708_add_project_commands"),
|
||||||
import("./migration/20260225215848_workspace"),
|
import("./migration/20260213144116_wakeful_the_professor"),
|
||||||
import("./migration/20260227213759_add_session_workspace_id"),
|
import("./migration/20260225215848_workspace"),
|
||||||
import("./migration/20260228203230_blue_harpoon"),
|
import("./migration/20260227213759_add_session_workspace_id"),
|
||||||
import("./migration/20260303231226_add_workspace_fields"),
|
import("./migration/20260228203230_blue_harpoon"),
|
||||||
import("./migration/20260309230000_move_org_to_state"),
|
import("./migration/20260303231226_add_workspace_fields"),
|
||||||
import("./migration/20260312043431_session_message_cursor"),
|
import("./migration/20260309230000_move_org_to_state"),
|
||||||
import("./migration/20260323234822_events"),
|
import("./migration/20260312043431_session_message_cursor"),
|
||||||
import("./migration/20260410174513_workspace-name"),
|
import("./migration/20260323234822_events"),
|
||||||
import("./migration/20260413175956_chief_energizer"),
|
import("./migration/20260410174513_workspace-name"),
|
||||||
import("./migration/20260423070820_add_icon_url_override"),
|
import("./migration/20260413175956_chief_energizer"),
|
||||||
import("./migration/20260427172553_slow_nightmare"),
|
import("./migration/20260423070820_add_icon_url_override"),
|
||||||
import("./migration/20260428004200_add_session_path"),
|
import("./migration/20260427172553_slow_nightmare"),
|
||||||
import("./migration/20260501142318_next_venus"),
|
import("./migration/20260428004200_add_session_path"),
|
||||||
import("./migration/20260504145000_add_sync_owner"),
|
import("./migration/20260501142318_next_venus"),
|
||||||
import("./migration/20260507164347_add_workspace_time"),
|
import("./migration/20260504145000_add_sync_owner"),
|
||||||
import("./migration/20260510033149_session_usage"),
|
import("./migration/20260507164347_add_workspace_time"),
|
||||||
import("./migration/20260511000411_data_migration_state"),
|
import("./migration/20260510033149_session_usage"),
|
||||||
import("./migration/20260530232709_lovely_romulus"),
|
import("./migration/20260511000411_data_migration_state"),
|
||||||
])).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
import("./migration/20260530232709_lovely_romulus"),
|
||||||
|
])
|
||||||
|
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ export default {
|
||||||
up(tx) {
|
up(tx) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* tx.run(`ALTER TABLE \`account_state\` ADD \`active_org_id\` text;`)
|
yield* tx.run(`ALTER TABLE \`account_state\` ADD \`active_org_id\` text;`)
|
||||||
yield* tx.run(`UPDATE \`account_state\` SET \`active_org_id\` = (SELECT \`selected_org_id\` FROM \`account\` WHERE \`account\`.\`id\` = \`account_state\`.\`active_account_id\`);`)
|
yield* tx.run(
|
||||||
|
`UPDATE \`account_state\` SET \`active_org_id\` = (SELECT \`selected_org_id\` FROM \`account\` WHERE \`account\`.\`id\` = \`account_state\`.\`active_account_id\`);`,
|
||||||
|
)
|
||||||
yield* tx.run(`ALTER TABLE \`account\` DROP COLUMN \`selected_org_id\`;`)
|
yield* tx.run(`ALTER TABLE \`account\` DROP COLUMN \`selected_org_id\`;`)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ export default {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* tx.run(`DROP INDEX IF EXISTS \`message_session_idx\`;`)
|
yield* tx.run(`DROP INDEX IF EXISTS \`message_session_idx\`;`)
|
||||||
yield* tx.run(`DROP INDEX IF EXISTS \`part_message_idx\`;`)
|
yield* tx.run(`DROP INDEX IF EXISTS \`part_message_idx\`;`)
|
||||||
yield* tx.run(`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`)
|
yield* tx.run(
|
||||||
|
`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`,
|
||||||
|
)
|
||||||
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
|
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ export default {
|
||||||
CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
`)
|
`)
|
||||||
yield* tx.run(`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`)
|
yield* tx.run(
|
||||||
|
`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`,
|
||||||
|
)
|
||||||
yield* tx.run(`DROP TABLE \`workspace\`;`)
|
yield* tx.run(`DROP TABLE \`workspace\`;`)
|
||||||
yield* tx.run(`ALTER TABLE \`__new_workspace\` RENAME TO \`workspace\`;`)
|
yield* tx.run(`ALTER TABLE \`__new_workspace\` RENAME TO \`workspace\`;`)
|
||||||
yield* tx.run(`PRAGMA foreign_keys=ON;`)
|
yield* tx.run(`PRAGMA foreign_keys=ON;`)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ export default {
|
||||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_session_type_idx\`;`)
|
yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_session_type_idx\`;`)
|
||||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_time_created_idx\`;`)
|
yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_time_created_idx\`;`)
|
||||||
yield* tx.run(`CREATE INDEX \`session_message_session_idx\` ON \`session_message\` (\`session_id\`);`)
|
yield* tx.run(`CREATE INDEX \`session_message_session_idx\` ON \`session_message\` (\`session_id\`);`)
|
||||||
yield* tx.run(`CREATE INDEX \`session_message_session_type_idx\` ON \`session_message\` (\`session_id\`,\`type\`);`)
|
yield* tx.run(
|
||||||
|
`CREATE INDEX \`session_message_session_type_idx\` ON \`session_message\` (\`session_id\`,\`type\`);`,
|
||||||
|
)
|
||||||
yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`)
|
yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`)
|
||||||
yield* tx.run(`DROP TABLE \`session_entry\`;`)
|
yield* tx.run(`DROP TABLE \`session_entry\`;`)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,9 @@ const make = (options: Config) =>
|
||||||
const native = (yield* Sqlite.Native) as Database
|
const native = (yield* Sqlite.Native) as Database
|
||||||
|
|
||||||
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames)
|
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames)
|
||||||
const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : undefined
|
const transformRows = options.transformResultNames
|
||||||
|
? Statement.defaultTransforms(options.transformResultNames).array
|
||||||
|
: undefined
|
||||||
|
|
||||||
const run = (query: string, params: ReadonlyArray<unknown> = []) =>
|
const run = (query: string, params: ReadonlyArray<unknown> = []) =>
|
||||||
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
|
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
|
||||||
|
|
@ -102,7 +104,9 @@ const make = (options: Config) =>
|
||||||
export: Effect.try({
|
export: Effect.try({
|
||||||
try: () => native.serialize(),
|
try: () => native.serialize(),
|
||||||
catch: (cause) =>
|
catch: (cause) =>
|
||||||
new SqlError({ reason: classifySqliteError(cause, { message: "Failed to export database", operation: "export" }) }),
|
new SqlError({
|
||||||
|
reason: classifySqliteError(cause, { message: "Failed to export database", operation: "export" }),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
loadExtension: (path) =>
|
loadExtension: (path) =>
|
||||||
Effect.try({
|
Effect.try({
|
||||||
|
|
@ -119,7 +123,10 @@ const make = (options: Config) =>
|
||||||
const transactionAcquirer = Effect.uninterruptibleMask((restore) => {
|
const transactionAcquirer = Effect.uninterruptibleMask((restore) => {
|
||||||
const fiber = Fiber.getCurrent()!
|
const fiber = Fiber.getCurrent()!
|
||||||
const scope = Context.getUnsafe(fiber.context, Scope.Scope)
|
const scope = Context.getUnsafe(fiber.context, Scope.Scope)
|
||||||
return Effect.as(Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), connection)
|
return Effect.as(
|
||||||
|
Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))),
|
||||||
|
connection,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const client = Object.assign(
|
const client = Object.assign(
|
||||||
|
|
@ -172,6 +179,4 @@ export const layer = (config: Config) =>
|
||||||
Layer.merge(
|
Layer.merge(
|
||||||
nativeLayer(config),
|
nativeLayer(config),
|
||||||
Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(nativeLayer(config))),
|
Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(nativeLayer(config))),
|
||||||
).pipe(
|
).pipe(Layer.provide(Reactivity.layer))
|
||||||
Layer.provide(Reactivity.layer),
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,9 @@ const make = (options: Config) =>
|
||||||
const native = (yield* Sqlite.Native) as DatabaseSync
|
const native = (yield* Sqlite.Native) as DatabaseSync
|
||||||
|
|
||||||
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames)
|
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames)
|
||||||
const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : undefined
|
const transformRows = options.transformResultNames
|
||||||
|
? Statement.defaultTransforms(options.transformResultNames).array
|
||||||
|
: undefined
|
||||||
|
|
||||||
const run = (query: string, params: ReadonlyArray<unknown> = []) =>
|
const run = (query: string, params: ReadonlyArray<unknown> = []) =>
|
||||||
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
|
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
|
||||||
|
|
@ -72,7 +74,9 @@ const make = (options: Config) =>
|
||||||
statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers))
|
statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers))
|
||||||
statement.setReturnArrays(true)
|
statement.setReturnArrays(true)
|
||||||
try {
|
try {
|
||||||
return Effect.succeed(statement.all(...(params as SQLInputValue[])) as unknown as ReadonlyArray<ReadonlyArray<unknown>>)
|
return Effect.succeed(
|
||||||
|
statement.all(...(params as SQLInputValue[])) as unknown as ReadonlyArray<ReadonlyArray<unknown>>,
|
||||||
|
)
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
return Effect.fail(
|
return Effect.fail(
|
||||||
new SqlError({
|
new SqlError({
|
||||||
|
|
@ -113,7 +117,10 @@ const make = (options: Config) =>
|
||||||
const transactionAcquirer = Effect.uninterruptibleMask((restore) => {
|
const transactionAcquirer = Effect.uninterruptibleMask((restore) => {
|
||||||
const fiber = Fiber.getCurrent()!
|
const fiber = Fiber.getCurrent()!
|
||||||
const scope = Context.getUnsafe(fiber.context, Scope.Scope)
|
const scope = Context.getUnsafe(fiber.context, Scope.Scope)
|
||||||
return Effect.as(Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), connection)
|
return Effect.as(
|
||||||
|
Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))),
|
||||||
|
connection,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const client = Object.assign(
|
const client = Object.assign(
|
||||||
|
|
@ -167,6 +174,4 @@ export const layer = (config: Config) =>
|
||||||
Layer.merge(
|
Layer.merge(
|
||||||
nativeLayer(config),
|
nativeLayer(config),
|
||||||
Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(nativeLayer(config))),
|
Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(nativeLayer(config))),
|
||||||
).pipe(
|
).pipe(Layer.provide(Reactivity.layer))
|
||||||
Layer.provide(Reactivity.layer),
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -261,7 +261,8 @@ export const layer = Layer.effect(
|
||||||
function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) {
|
function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
|
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
|
||||||
const location = options?.location ??
|
const location =
|
||||||
|
options?.location ??
|
||||||
(serviceLocation
|
(serviceLocation
|
||||||
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
|
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
|
||||||
: undefined)
|
: undefined)
|
||||||
|
|
|
||||||
|
|
@ -103,371 +103,371 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||||
|
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* SessionEvent.All.match(event, {
|
yield* SessionEvent.All.match(event, {
|
||||||
"session.next.agent.switched": (event) => {
|
"session.next.agent.switched": (event) => {
|
||||||
return adapter.appendMessage(
|
return adapter.appendMessage(
|
||||||
new SessionMessage.AgentSwitched({
|
new SessionMessage.AgentSwitched({
|
||||||
id: event.id,
|
id: event.id,
|
||||||
type: "agent-switched",
|
type: "agent-switched",
|
||||||
metadata: event.metadata,
|
metadata: event.metadata,
|
||||||
agent: event.data.agent,
|
agent: event.data.agent,
|
||||||
time: { created: event.data.timestamp },
|
time: { created: event.data.timestamp },
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
"session.next.model.switched": (event) => {
|
|
||||||
return adapter.appendMessage(
|
|
||||||
new SessionMessage.ModelSwitched({
|
|
||||||
id: event.id,
|
|
||||||
type: "model-switched",
|
|
||||||
metadata: event.metadata,
|
|
||||||
model: event.data.model,
|
|
||||||
time: { created: event.data.timestamp },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
"session.next.prompted": (event) => {
|
|
||||||
return adapter.appendMessage(
|
|
||||||
new SessionMessage.User({
|
|
||||||
id: event.id,
|
|
||||||
type: "user",
|
|
||||||
metadata: event.metadata,
|
|
||||||
text: event.data.prompt.text,
|
|
||||||
files: event.data.prompt.files,
|
|
||||||
agents: event.data.prompt.agents,
|
|
||||||
references: event.data.prompt.references,
|
|
||||||
time: { created: event.data.timestamp },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
"session.next.synthetic": (event) => {
|
|
||||||
return adapter.appendMessage(
|
|
||||||
new SessionMessage.Synthetic({
|
|
||||||
sessionID: event.data.sessionID,
|
|
||||||
text: event.data.text,
|
|
||||||
id: event.id,
|
|
||||||
type: "synthetic",
|
|
||||||
time: { created: event.data.timestamp },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
"session.next.shell.started": (event) => {
|
|
||||||
return adapter.appendMessage(
|
|
||||||
new SessionMessage.Shell({
|
|
||||||
id: event.id,
|
|
||||||
type: "shell",
|
|
||||||
metadata: event.metadata,
|
|
||||||
callID: event.data.callID,
|
|
||||||
command: event.data.command,
|
|
||||||
output: "",
|
|
||||||
time: { created: event.data.timestamp },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
"session.next.shell.ended": (event) => {
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const currentShell = yield* adapter.getCurrentShell(event.data.callID)
|
|
||||||
if (currentShell) {
|
|
||||||
yield* adapter.updateShell(
|
|
||||||
produce(currentShell, (draft) => {
|
|
||||||
draft.output = event.data.output
|
|
||||||
draft.time.completed = event.data.timestamp
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
})
|
"session.next.model.switched": (event) => {
|
||||||
},
|
return adapter.appendMessage(
|
||||||
"session.next.step.started": (event) => {
|
new SessionMessage.ModelSwitched({
|
||||||
return Effect.gen(function* () {
|
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
|
||||||
if (currentAssistant) {
|
|
||||||
yield* adapter.updateAssistant(
|
|
||||||
produce(currentAssistant, (draft) => {
|
|
||||||
draft.time.completed = event.data.timestamp
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
yield* adapter.appendMessage(
|
|
||||||
new SessionMessage.Assistant({
|
|
||||||
id: event.id,
|
id: event.id,
|
||||||
type: "assistant",
|
type: "model-switched",
|
||||||
agent: event.data.agent,
|
metadata: event.metadata,
|
||||||
model: event.data.model,
|
model: event.data.model,
|
||||||
time: { created: event.data.timestamp },
|
time: { created: event.data.timestamp },
|
||||||
content: [],
|
|
||||||
snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined,
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
},
|
"session.next.prompted": (event) => {
|
||||||
"session.next.step.ended": (event) => {
|
return adapter.appendMessage(
|
||||||
return Effect.gen(function* () {
|
new SessionMessage.User({
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
id: event.id,
|
||||||
if (currentAssistant) {
|
type: "user",
|
||||||
yield* adapter.updateAssistant(
|
metadata: event.metadata,
|
||||||
produce(currentAssistant, (draft) => {
|
text: event.data.prompt.text,
|
||||||
draft.time.completed = event.data.timestamp
|
files: event.data.prompt.files,
|
||||||
draft.finish = event.data.finish
|
agents: event.data.prompt.agents,
|
||||||
draft.cost = event.data.cost
|
references: event.data.prompt.references,
|
||||||
draft.tokens = event.data.tokens
|
time: { created: event.data.timestamp },
|
||||||
if (event.data.snapshot) draft.snapshot = { ...draft.snapshot, end: event.data.snapshot }
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
})
|
"session.next.synthetic": (event) => {
|
||||||
},
|
return adapter.appendMessage(
|
||||||
"session.next.step.failed": (event) => {
|
new SessionMessage.Synthetic({
|
||||||
return Effect.gen(function* () {
|
sessionID: event.data.sessionID,
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
text: event.data.text,
|
||||||
if (currentAssistant) {
|
id: event.id,
|
||||||
yield* adapter.updateAssistant(
|
type: "synthetic",
|
||||||
produce(currentAssistant, (draft) => {
|
time: { created: event.data.timestamp },
|
||||||
draft.time.completed = event.data.timestamp
|
|
||||||
draft.finish = "error"
|
|
||||||
draft.error = event.data.error
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
})
|
"session.next.shell.started": (event) => {
|
||||||
},
|
return adapter.appendMessage(
|
||||||
"session.next.text.started": () => {
|
new SessionMessage.Shell({
|
||||||
return Effect.gen(function* () {
|
id: event.id,
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
type: "shell",
|
||||||
if (currentAssistant) {
|
metadata: event.metadata,
|
||||||
yield* adapter.updateAssistant(
|
callID: event.data.callID,
|
||||||
produce(currentAssistant, (draft) => {
|
command: event.data.command,
|
||||||
draft.content.push(new SessionMessage.AssistantText({ type: "text", text: "" }) as DraftText)
|
output: "",
|
||||||
|
time: { created: event.data.timestamp },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
})
|
"session.next.shell.ended": (event) => {
|
||||||
},
|
return Effect.gen(function* () {
|
||||||
"session.next.text.delta": (event) => {
|
const currentShell = yield* adapter.getCurrentShell(event.data.callID)
|
||||||
return Effect.gen(function* () {
|
if (currentShell) {
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
yield* adapter.updateShell(
|
||||||
if (currentAssistant) {
|
produce(currentShell, (draft) => {
|
||||||
yield* adapter.updateAssistant(
|
draft.output = event.data.output
|
||||||
produce(currentAssistant, (draft) => {
|
draft.time.completed = event.data.timestamp
|
||||||
const match = latestText(draft)
|
}),
|
||||||
if (match) match.text += event.data.delta
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
"session.next.text.ended": (event) => {
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
|
||||||
if (currentAssistant) {
|
|
||||||
yield* adapter.updateAssistant(
|
|
||||||
produce(currentAssistant, (draft) => {
|
|
||||||
const match = latestText(draft)
|
|
||||||
if (match) match.text = event.data.text
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
"session.next.tool.input.started": (event) => {
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
|
||||||
if (currentAssistant) {
|
|
||||||
yield* adapter.updateAssistant(
|
|
||||||
produce(currentAssistant, (draft) => {
|
|
||||||
draft.content.push(
|
|
||||||
new SessionMessage.AssistantTool({
|
|
||||||
type: "tool",
|
|
||||||
id: event.data.callID,
|
|
||||||
name: event.data.name,
|
|
||||||
time: { created: event.data.timestamp },
|
|
||||||
state: new SessionMessage.ToolStatePending({ status: "pending", input: "" }),
|
|
||||||
}) as DraftTool,
|
|
||||||
)
|
)
|
||||||
}),
|
}
|
||||||
)
|
})
|
||||||
}
|
},
|
||||||
})
|
"session.next.step.started": (event) => {
|
||||||
},
|
return Effect.gen(function* () {
|
||||||
"session.next.tool.input.delta": (event) => {
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
return Effect.gen(function* () {
|
if (currentAssistant) {
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
yield* adapter.updateAssistant(
|
||||||
if (currentAssistant) {
|
produce(currentAssistant, (draft) => {
|
||||||
yield* adapter.updateAssistant(
|
draft.time.completed = event.data.timestamp
|
||||||
produce(currentAssistant, (draft) => {
|
}),
|
||||||
const match = latestTool(draft, event.data.callID)
|
|
||||||
// oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string)
|
|
||||||
if (match && match.state.status === "pending") match.state.input += event.data.delta
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
"session.next.tool.input.ended": () => Effect.void,
|
|
||||||
"session.next.tool.called": (event) => {
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
|
||||||
if (currentAssistant) {
|
|
||||||
yield* adapter.updateAssistant(
|
|
||||||
produce(currentAssistant, (draft) => {
|
|
||||||
const match = latestTool(draft, event.data.callID)
|
|
||||||
if (match) {
|
|
||||||
match.provider = event.data.provider
|
|
||||||
match.time.ran = event.data.timestamp
|
|
||||||
match.state = {
|
|
||||||
status: "running",
|
|
||||||
input: event.data.input,
|
|
||||||
structured: {},
|
|
||||||
content: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
"session.next.tool.progress": (event) => {
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
|
||||||
if (currentAssistant) {
|
|
||||||
yield* adapter.updateAssistant(
|
|
||||||
produce(currentAssistant, (draft) => {
|
|
||||||
const match = latestTool(draft, event.data.callID)
|
|
||||||
if (match && match.state.status === "running") {
|
|
||||||
match.state.structured = event.data.structured
|
|
||||||
match.state.content = [...event.data.content]
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
"session.next.tool.success": (event) => {
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
|
||||||
if (currentAssistant) {
|
|
||||||
yield* adapter.updateAssistant(
|
|
||||||
produce(currentAssistant, (draft) => {
|
|
||||||
const match = latestTool(draft, event.data.callID)
|
|
||||||
if (match && match.state.status === "running") {
|
|
||||||
match.provider = event.data.provider
|
|
||||||
match.time.completed = event.data.timestamp
|
|
||||||
match.state = {
|
|
||||||
status: "completed",
|
|
||||||
input: match.state.input,
|
|
||||||
structured: event.data.structured,
|
|
||||||
content: [...event.data.content],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
"session.next.tool.failed": (event) => {
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
|
||||||
if (currentAssistant) {
|
|
||||||
yield* adapter.updateAssistant(
|
|
||||||
produce(currentAssistant, (draft) => {
|
|
||||||
const match = latestTool(draft, event.data.callID)
|
|
||||||
if (match && match.state.status === "running") {
|
|
||||||
match.provider = event.data.provider
|
|
||||||
match.time.completed = event.data.timestamp
|
|
||||||
match.state = {
|
|
||||||
status: "error",
|
|
||||||
error: event.data.error,
|
|
||||||
input: match.state.input,
|
|
||||||
structured: match.state.structured,
|
|
||||||
content: match.state.content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
"session.next.reasoning.started": (event) => {
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
|
||||||
if (currentAssistant) {
|
|
||||||
yield* adapter.updateAssistant(
|
|
||||||
produce(currentAssistant, (draft) => {
|
|
||||||
draft.content.push(
|
|
||||||
new SessionMessage.AssistantReasoning({
|
|
||||||
type: "reasoning",
|
|
||||||
id: event.data.reasoningID,
|
|
||||||
text: "",
|
|
||||||
}) as DraftReasoning,
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
yield* adapter.appendMessage(
|
||||||
|
new SessionMessage.Assistant({
|
||||||
|
id: event.id,
|
||||||
|
type: "assistant",
|
||||||
|
agent: event.data.agent,
|
||||||
|
model: event.data.model,
|
||||||
|
time: { created: event.data.timestamp },
|
||||||
|
content: [],
|
||||||
|
snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.step.ended": (event) => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
draft.time.completed = event.data.timestamp
|
||||||
|
draft.finish = event.data.finish
|
||||||
|
draft.cost = event.data.cost
|
||||||
|
draft.tokens = event.data.tokens
|
||||||
|
if (event.data.snapshot) draft.snapshot = { ...draft.snapshot, end: event.data.snapshot }
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.step.failed": (event) => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
draft.time.completed = event.data.timestamp
|
||||||
|
draft.finish = "error"
|
||||||
|
draft.error = event.data.error
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.text.started": () => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
draft.content.push(new SessionMessage.AssistantText({ type: "text", text: "" }) as DraftText)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.text.delta": (event) => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
const match = latestText(draft)
|
||||||
|
if (match) match.text += event.data.delta
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.text.ended": (event) => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
const match = latestText(draft)
|
||||||
|
if (match) match.text = event.data.text
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.tool.input.started": (event) => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
draft.content.push(
|
||||||
|
new SessionMessage.AssistantTool({
|
||||||
|
type: "tool",
|
||||||
|
id: event.data.callID,
|
||||||
|
name: event.data.name,
|
||||||
|
time: { created: event.data.timestamp },
|
||||||
|
state: new SessionMessage.ToolStatePending({ status: "pending", input: "" }),
|
||||||
|
}) as DraftTool,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.tool.input.delta": (event) => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
const match = latestTool(draft, event.data.callID)
|
||||||
|
// oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string)
|
||||||
|
if (match && match.state.status === "pending") match.state.input += event.data.delta
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.tool.input.ended": () => Effect.void,
|
||||||
|
"session.next.tool.called": (event) => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
const match = latestTool(draft, event.data.callID)
|
||||||
|
if (match) {
|
||||||
|
match.provider = event.data.provider
|
||||||
|
match.time.ran = event.data.timestamp
|
||||||
|
match.state = {
|
||||||
|
status: "running",
|
||||||
|
input: event.data.input,
|
||||||
|
structured: {},
|
||||||
|
content: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.tool.progress": (event) => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
const match = latestTool(draft, event.data.callID)
|
||||||
|
if (match && match.state.status === "running") {
|
||||||
|
match.state.structured = event.data.structured
|
||||||
|
match.state.content = [...event.data.content]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.tool.success": (event) => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
const match = latestTool(draft, event.data.callID)
|
||||||
|
if (match && match.state.status === "running") {
|
||||||
|
match.provider = event.data.provider
|
||||||
|
match.time.completed = event.data.timestamp
|
||||||
|
match.state = {
|
||||||
|
status: "completed",
|
||||||
|
input: match.state.input,
|
||||||
|
structured: event.data.structured,
|
||||||
|
content: [...event.data.content],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.tool.failed": (event) => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
const match = latestTool(draft, event.data.callID)
|
||||||
|
if (match && match.state.status === "running") {
|
||||||
|
match.provider = event.data.provider
|
||||||
|
match.time.completed = event.data.timestamp
|
||||||
|
match.state = {
|
||||||
|
status: "error",
|
||||||
|
error: event.data.error,
|
||||||
|
input: match.state.input,
|
||||||
|
structured: match.state.structured,
|
||||||
|
content: match.state.content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.reasoning.started": (event) => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
draft.content.push(
|
||||||
|
new SessionMessage.AssistantReasoning({
|
||||||
|
type: "reasoning",
|
||||||
|
id: event.data.reasoningID,
|
||||||
|
text: "",
|
||||||
|
}) as DraftReasoning,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.reasoning.delta": (event) => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
const match = latestReasoning(draft, event.data.reasoningID)
|
||||||
|
if (match) match.text += event.data.delta
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.reasoning.ended": (event) => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const currentAssistant = yield* adapter.getCurrentAssistant()
|
||||||
|
if (currentAssistant) {
|
||||||
|
yield* adapter.updateAssistant(
|
||||||
|
produce(currentAssistant, (draft) => {
|
||||||
|
const match = latestReasoning(draft, event.data.reasoningID)
|
||||||
|
if (match) match.text = event.data.text
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"session.next.retried": () => Effect.void,
|
||||||
|
"session.next.compaction.started": (event) => {
|
||||||
|
return adapter.appendMessage(
|
||||||
|
new SessionMessage.Compaction({
|
||||||
|
id: event.id,
|
||||||
|
type: "compaction",
|
||||||
|
metadata: event.metadata,
|
||||||
|
reason: event.data.reason,
|
||||||
|
summary: "",
|
||||||
|
time: { created: event.data.timestamp },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
})
|
"session.next.compaction.delta": (event) => {
|
||||||
},
|
return Effect.gen(function* () {
|
||||||
"session.next.reasoning.delta": (event) => {
|
const currentCompaction = yield* adapter.getCurrentCompaction()
|
||||||
return Effect.gen(function* () {
|
if (currentCompaction) {
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
yield* adapter.updateCompaction(
|
||||||
if (currentAssistant) {
|
produce(currentCompaction, (draft) => {
|
||||||
yield* adapter.updateAssistant(
|
draft.summary += event.data.text
|
||||||
produce(currentAssistant, (draft) => {
|
}),
|
||||||
const match = latestReasoning(draft, event.data.reasoningID)
|
)
|
||||||
if (match) match.text += event.data.delta
|
}
|
||||||
}),
|
})
|
||||||
)
|
},
|
||||||
}
|
"session.next.compaction.ended": (event) => {
|
||||||
})
|
return Effect.gen(function* () {
|
||||||
},
|
const currentCompaction = yield* adapter.getCurrentCompaction()
|
||||||
"session.next.reasoning.ended": (event) => {
|
if (currentCompaction) {
|
||||||
return Effect.gen(function* () {
|
yield* adapter.updateCompaction(
|
||||||
const currentAssistant = yield* adapter.getCurrentAssistant()
|
produce(currentCompaction, (draft) => {
|
||||||
if (currentAssistant) {
|
draft.summary = event.data.text
|
||||||
yield* adapter.updateAssistant(
|
draft.include = event.data.include
|
||||||
produce(currentAssistant, (draft) => {
|
}),
|
||||||
const match = latestReasoning(draft, event.data.reasoningID)
|
)
|
||||||
if (match) match.text = event.data.text
|
}
|
||||||
}),
|
})
|
||||||
)
|
},
|
||||||
}
|
})
|
||||||
})
|
|
||||||
},
|
|
||||||
"session.next.retried": () => Effect.void,
|
|
||||||
"session.next.compaction.started": (event) => {
|
|
||||||
return adapter.appendMessage(
|
|
||||||
new SessionMessage.Compaction({
|
|
||||||
id: event.id,
|
|
||||||
type: "compaction",
|
|
||||||
metadata: event.metadata,
|
|
||||||
reason: event.data.reason,
|
|
||||||
summary: "",
|
|
||||||
time: { created: event.data.timestamp },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
"session.next.compaction.delta": (event) => {
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const currentCompaction = yield* adapter.getCurrentCompaction()
|
|
||||||
if (currentCompaction) {
|
|
||||||
yield* adapter.updateCompaction(
|
|
||||||
produce(currentCompaction, (draft) => {
|
|
||||||
draft.summary += event.data.text
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
"session.next.compaction.ended": (event) => {
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const currentCompaction = yield* adapter.getCurrentCompaction()
|
|
||||||
if (currentCompaction) {
|
|
||||||
yield* adapter.updateCompaction(
|
|
||||||
produce(currentCompaction, (draft) => {
|
|
||||||
draft.summary = event.data.text
|
|
||||||
draft.include = event.data.include
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,7 @@ const locationLayer = Layer.succeed(
|
||||||
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
|
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
|
||||||
)
|
)
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
Catalog.locationLayer.pipe(
|
Catalog.locationLayer.pipe(Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge(locationLayer)),
|
||||||
Layer.provideMerge(EventV2.defaultLayer),
|
|
||||||
Layer.provideMerge(locationLayer),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
describe("CatalogV2", () => {
|
describe("CatalogV2", () => {
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,18 @@ import sessionUsageMigration from "@opencode-ai/core/database/migration/20260510
|
||||||
import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
|
import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
|
||||||
|
|
||||||
const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
|
const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
|
||||||
Effect.runPromise(effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped))
|
Effect.runPromise(
|
||||||
|
effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped),
|
||||||
|
)
|
||||||
|
|
||||||
const makeDb = EffectDrizzleSqlite.makeWithDefaults()
|
const makeDb = EffectDrizzleSqlite.makeWithDefaults()
|
||||||
|
|
||||||
describe("DatabaseMigration", () => {
|
describe("DatabaseMigration", () => {
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
test("declared schema has no ungenerated migrations", async () => {
|
test("declared schema has no ungenerated migrations", async () => {
|
||||||
const result = await $`bun ${fileURLToPath(new URL("../script/migration.ts", import.meta.url))} --check`.quiet().nothrow()
|
const result = await $`bun ${fileURLToPath(new URL("../script/migration.ts", import.meta.url))} --check`
|
||||||
|
.quiet()
|
||||||
|
.nothrow()
|
||||||
expect(result.exitCode, result.stderr.toString()).toBe(0)
|
expect(result.exitCode, result.stderr.toString()).toBe(0)
|
||||||
expect(result.stdout.toString()).toContain("No schema changes, nothing to migrate")
|
expect(result.stdout.toString()).toContain("No schema changes, nothing to migrate")
|
||||||
}, 30_000)
|
}, 30_000)
|
||||||
|
|
@ -70,7 +74,9 @@ describe("DatabaseMigration", () => {
|
||||||
await run(
|
await run(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const db = yield* makeDb
|
const db = yield* makeDb
|
||||||
yield* db.run(sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`)
|
yield* db.run(
|
||||||
|
sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`,
|
||||||
|
)
|
||||||
yield* db.run(sql`
|
yield* db.run(sql`
|
||||||
INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at)
|
INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at)
|
||||||
VALUES ('hash', 1, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()})
|
VALUES ('hash', 1, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()})
|
||||||
|
|
@ -89,7 +95,9 @@ describe("DatabaseMigration", () => {
|
||||||
const db = yield* makeDb
|
const db = yield* makeDb
|
||||||
yield* db.run(sql`CREATE TABLE migration (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`)
|
yield* db.run(sql`CREATE TABLE migration (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`)
|
||||||
yield* db.run(sql`INSERT INTO migration (id, time_completed) VALUES ('existing', 1)`)
|
yield* db.run(sql`INSERT INTO migration (id, time_completed) VALUES ('existing', 1)`)
|
||||||
yield* db.run(sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`)
|
yield* db.run(
|
||||||
|
sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`,
|
||||||
|
)
|
||||||
yield* db.run(sql`
|
yield* db.run(sql`
|
||||||
INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at)
|
INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at)
|
||||||
VALUES ('hash', 1, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()})
|
VALUES ('hash', 1, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()})
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,12 @@ describe("EventV2", () => {
|
||||||
const aggregateID = EventV2.ID.create()
|
const aggregateID = EventV2.ID.create()
|
||||||
|
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "first" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "first" })
|
||||||
const rows = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all().pipe(Effect.orDie)
|
const rows = yield* db
|
||||||
|
.select()
|
||||||
|
.from(EventTable)
|
||||||
|
.where(eq(EventTable.aggregate_id, aggregateID))
|
||||||
|
.all()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
|
||||||
expect(rows).toHaveLength(1)
|
expect(rows).toHaveLength(1)
|
||||||
expect(rows[0]?.type).toBe(EventV2.versionedType(SyncMessage.type, 1))
|
expect(rows[0]?.type).toBe(EventV2.versionedType(SyncMessage.type, 1))
|
||||||
|
|
@ -227,7 +232,12 @@ describe("EventV2", () => {
|
||||||
|
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "first" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "first" })
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "second" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "second" })
|
||||||
const rows = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all().pipe(Effect.orDie)
|
const rows = yield* db
|
||||||
|
.select()
|
||||||
|
.from(EventTable)
|
||||||
|
.where(eq(EventTable.aggregate_id, aggregateID))
|
||||||
|
.all()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
|
||||||
expect(rows.map((row) => row.seq)).toEqual([0, 1])
|
expect(rows.map((row) => row.seq)).toEqual([0, 1])
|
||||||
}),
|
}),
|
||||||
|
|
@ -240,7 +250,12 @@ describe("EventV2", () => {
|
||||||
const aggregateID = EventV2.ID.create()
|
const aggregateID = EventV2.ID.create()
|
||||||
|
|
||||||
yield* events.publish(SyncSent, { messageID: aggregateID, text: "sent" })
|
yield* events.publish(SyncSent, { messageID: aggregateID, text: "sent" })
|
||||||
const rows = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all().pipe(Effect.orDie)
|
const rows = yield* db
|
||||||
|
.select()
|
||||||
|
.from(EventTable)
|
||||||
|
.where(eq(EventTable.aggregate_id, aggregateID))
|
||||||
|
.all()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
|
||||||
expect(rows).toHaveLength(1)
|
expect(rows).toHaveLength(1)
|
||||||
expect(rows[0]?.aggregate_id).toBe(aggregateID)
|
expect(rows[0]?.aggregate_id).toBe(aggregateID)
|
||||||
|
|
@ -284,7 +299,12 @@ describe("EventV2", () => {
|
||||||
aggregateID,
|
aggregateID,
|
||||||
data: { id: aggregateID, text: "replayed" },
|
data: { id: aggregateID, text: "replayed" },
|
||||||
})
|
})
|
||||||
const rows = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all().pipe(Effect.orDie)
|
const rows = yield* db
|
||||||
|
.select()
|
||||||
|
.from(EventTable)
|
||||||
|
.where(eq(EventTable.aggregate_id, aggregateID))
|
||||||
|
.all()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
|
||||||
expect(rows).toHaveLength(1)
|
expect(rows).toHaveLength(1)
|
||||||
expect(rows[0]?.aggregate_id).toBe(aggregateID)
|
expect(rows[0]?.aggregate_id).toBe(aggregateID)
|
||||||
|
|
@ -397,7 +417,12 @@ describe("EventV2", () => {
|
||||||
data: { id: aggregateID, text: "four" },
|
data: { id: aggregateID, text: "four" },
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
const rows = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all().pipe(Effect.orDie)
|
const rows = yield* db
|
||||||
|
.select()
|
||||||
|
.from(EventTable)
|
||||||
|
.where(eq(EventTable.aggregate_id, aggregateID))
|
||||||
|
.all()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
|
||||||
expect(one).toBe(aggregateID)
|
expect(one).toBe(aggregateID)
|
||||||
expect(two).toBe(aggregateID)
|
expect(two).toBe(aggregateID)
|
||||||
|
|
@ -486,7 +511,12 @@ describe("EventV2", () => {
|
||||||
},
|
},
|
||||||
{ ownerID: "owner-2" },
|
{ ownerID: "owner-2" },
|
||||||
)
|
)
|
||||||
const rows = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all().pipe(Effect.orDie)
|
const rows = yield* db
|
||||||
|
.select()
|
||||||
|
.from(EventTable)
|
||||||
|
.where(eq(EventTable.aggregate_id, aggregateID))
|
||||||
|
.all()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
const sequence = yield* db
|
const sequence = yield* db
|
||||||
.select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
|
.select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
|
||||||
.from(EventSequenceTable)
|
.from(EventSequenceTable)
|
||||||
|
|
|
||||||
|
|
@ -162,5 +162,7 @@ export const make = (
|
||||||
|
|
||||||
export const layer = (config: SqliteClientConfig): Layer.Layer<SqliteClient | Client.SqlClient> =>
|
export const layer = (config: SqliteClientConfig): Layer.Layer<SqliteClient | Client.SqlClient> =>
|
||||||
Layer.effectContext(
|
Layer.effectContext(
|
||||||
Effect.map(make(config), (client) => Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client))),
|
Effect.map(make(config), (client) =>
|
||||||
|
Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)),
|
||||||
|
),
|
||||||
).pipe(Layer.provide(Reactivity.layer))
|
).pipe(Layer.provide(Reactivity.layer))
|
||||||
|
|
|
||||||
|
|
@ -48,17 +48,9 @@ export const layer = Layer.effect(
|
||||||
effect.pipe(Effect.mapError((cause) => new AccountRepoError({ message: "Database operation failed", cause })))
|
effect.pipe(Effect.mapError((cause) => new AccountRepoError({ message: "Database operation failed", cause })))
|
||||||
|
|
||||||
const current = Effect.fnUntraced(function* () {
|
const current = Effect.fnUntraced(function* () {
|
||||||
const state = yield* db
|
const state = yield* db.select().from(AccountStateTable).where(eq(AccountStateTable.id, ACCOUNT_STATE_ID)).get()
|
||||||
.select()
|
|
||||||
.from(AccountStateTable)
|
|
||||||
.where(eq(AccountStateTable.id, ACCOUNT_STATE_ID))
|
|
||||||
.get()
|
|
||||||
if (!state?.active_account_id) return
|
if (!state?.active_account_id) return
|
||||||
const account = yield* db
|
const account = yield* db.select().from(AccountTable).where(eq(AccountTable.id, state.active_account_id)).get()
|
||||||
.select()
|
|
||||||
.from(AccountTable)
|
|
||||||
.where(eq(AccountTable.id, state.active_account_id))
|
|
||||||
.get()
|
|
||||||
if (!account) return
|
if (!account) return
|
||||||
return { ...account, active_org_id: state.active_org_id ?? null }
|
return { ...account, active_org_id: state.active_org_id ?? null }
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,12 @@ export const layer = Layer.effect(
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
const state = yield* InstanceState.make<State>(
|
const state = yield* InstanceState.make<State>(
|
||||||
Effect.fn("Permission.state")(function* (ctx) {
|
Effect.fn("Permission.state")(function* (ctx) {
|
||||||
const row = yield* db.select().from(PermissionTable).where(eq(PermissionTable.project_id, ctx.project.id)).get().pipe(Effect.orDie)
|
const row = yield* db
|
||||||
|
.select()
|
||||||
|
.from(PermissionTable)
|
||||||
|
.where(eq(PermissionTable.project_id, ctx.project.id))
|
||||||
|
.get()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
const state = {
|
const state = {
|
||||||
pending: new Map<PermissionID, PendingEntry>(),
|
pending: new Map<PermissionID, PendingEntry>(),
|
||||||
approved: [...(row?.data ?? [])],
|
approved: [...(row?.data ?? [])],
|
||||||
|
|
|
||||||
|
|
@ -190,41 +190,57 @@ export const layer = Layer.effect(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const oldProject = yield* d.select().from(ProjectTable).where(eq(ProjectTable.id, oldID)).get()
|
const oldProject = yield* d.select().from(ProjectTable).where(eq(ProjectTable.id, oldID)).get()
|
||||||
const newProject = yield* d.select().from(ProjectTable).where(eq(ProjectTable.id, newID)).get()
|
const newProject = yield* d.select().from(ProjectTable).where(eq(ProjectTable.id, newID)).get()
|
||||||
if (oldProject && !newProject) {
|
if (oldProject && !newProject) {
|
||||||
yield* d
|
yield* d
|
||||||
.insert(ProjectTable)
|
.insert(ProjectTable)
|
||||||
.values({
|
.values({
|
||||||
...oldProject,
|
...oldProject,
|
||||||
id: newID,
|
id: newID,
|
||||||
time_updated: Date.now(),
|
time_updated: Date.now(),
|
||||||
})
|
})
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldPermission = yield* d.select().from(PermissionTable).where(eq(PermissionTable.project_id, oldID)).get()
|
const oldPermission = yield* d
|
||||||
const newPermission = yield* d.select().from(PermissionTable).where(eq(PermissionTable.project_id, newID)).get()
|
.select()
|
||||||
if (oldPermission && newPermission) {
|
.from(PermissionTable)
|
||||||
yield* d
|
.where(eq(PermissionTable.project_id, oldID))
|
||||||
.update(PermissionTable)
|
.get()
|
||||||
.set({
|
const newPermission = yield* d
|
||||||
data: mergePermissionRules(oldPermission.data, newPermission.data),
|
.select()
|
||||||
time_created: Math.min(oldPermission.time_created, newPermission.time_created),
|
.from(PermissionTable)
|
||||||
time_updated: Date.now(),
|
|
||||||
})
|
|
||||||
.where(eq(PermissionTable.project_id, newID))
|
.where(eq(PermissionTable.project_id, newID))
|
||||||
.run()
|
.get()
|
||||||
|
if (oldPermission && newPermission) {
|
||||||
|
yield* d
|
||||||
|
.update(PermissionTable)
|
||||||
|
.set({
|
||||||
|
data: mergePermissionRules(oldPermission.data, newPermission.data),
|
||||||
|
time_created: Math.min(oldPermission.time_created, newPermission.time_created),
|
||||||
|
time_updated: Date.now(),
|
||||||
|
})
|
||||||
|
.where(eq(PermissionTable.project_id, newID))
|
||||||
|
.run()
|
||||||
yield* d.delete(PermissionTable).where(eq(PermissionTable.project_id, oldID)).run()
|
yield* d.delete(PermissionTable).where(eq(PermissionTable.project_id, oldID)).run()
|
||||||
}
|
}
|
||||||
if (oldPermission && !newPermission) {
|
if (oldPermission && !newPermission) {
|
||||||
yield* d.update(PermissionTable).set({ project_id: newID }).where(eq(PermissionTable.project_id, oldID)).run()
|
yield* d
|
||||||
}
|
.update(PermissionTable)
|
||||||
|
.set({ project_id: newID })
|
||||||
|
.where(eq(PermissionTable.project_id, oldID))
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
yield* d
|
yield* d
|
||||||
.update(SessionTable)
|
.update(SessionTable)
|
||||||
.set({ project_id: newID, time_updated: sql`${SessionTable.time_updated}` })
|
.set({ project_id: newID, time_updated: sql`${SessionTable.time_updated}` })
|
||||||
.where(eq(SessionTable.project_id, oldID))
|
.where(eq(SessionTable.project_id, oldID))
|
||||||
.run()
|
.run()
|
||||||
yield* d.update(WorkspaceTable).set({ project_id: newID }).where(eq(WorkspaceTable.project_id, oldID)).run()
|
yield* d
|
||||||
|
.update(WorkspaceTable)
|
||||||
|
.set({ project_id: newID })
|
||||||
|
.where(eq(WorkspaceTable.project_id, oldID))
|
||||||
|
.run()
|
||||||
|
|
||||||
if (oldProject) yield* d.delete(ProjectTable).where(eq(ProjectTable.id, oldID)).run()
|
if (oldProject) yield* d.delete(ProjectTable).where(eq(ProjectTable.id, oldID)).run()
|
||||||
}),
|
}),
|
||||||
|
|
@ -278,46 +294,46 @@ export const layer = Layer.effect(
|
||||||
).pipe(Effect.map((arr) => arr.filter((x): x is string => x !== undefined)))
|
).pipe(Effect.map((arr) => arr.filter((x): x is string => x !== undefined)))
|
||||||
|
|
||||||
yield* db
|
yield* db
|
||||||
.insert(ProjectTable)
|
.insert(ProjectTable)
|
||||||
.values({
|
.values({
|
||||||
id: result.id,
|
id: result.id,
|
||||||
|
worktree: result.worktree,
|
||||||
|
vcs: result.vcs ?? null,
|
||||||
|
name: result.name,
|
||||||
|
icon_url: result.icon?.url,
|
||||||
|
icon_url_override: result.icon?.override,
|
||||||
|
icon_color: result.icon?.color,
|
||||||
|
time_created: result.time.created,
|
||||||
|
time_updated: result.time.updated,
|
||||||
|
time_initialized: result.time.initialized,
|
||||||
|
sandboxes: result.sandboxes,
|
||||||
|
commands: result.commands,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: ProjectTable.id,
|
||||||
|
set: {
|
||||||
worktree: result.worktree,
|
worktree: result.worktree,
|
||||||
vcs: result.vcs ?? null,
|
vcs: result.vcs ?? null,
|
||||||
name: result.name,
|
name: result.name,
|
||||||
icon_url: result.icon?.url,
|
icon_url: result.icon?.url,
|
||||||
icon_url_override: result.icon?.override,
|
icon_url_override: result.icon?.override,
|
||||||
icon_color: result.icon?.color,
|
icon_color: result.icon?.color,
|
||||||
time_created: result.time.created,
|
|
||||||
time_updated: result.time.updated,
|
time_updated: result.time.updated,
|
||||||
time_initialized: result.time.initialized,
|
time_initialized: result.time.initialized,
|
||||||
sandboxes: result.sandboxes,
|
sandboxes: result.sandboxes,
|
||||||
commands: result.commands,
|
commands: result.commands,
|
||||||
})
|
},
|
||||||
.onConflictDoUpdate({
|
})
|
||||||
target: ProjectTable.id,
|
.run()
|
||||||
set: {
|
.pipe(Effect.orDie)
|
||||||
worktree: result.worktree,
|
|
||||||
vcs: result.vcs ?? null,
|
|
||||||
name: result.name,
|
|
||||||
icon_url: result.icon?.url,
|
|
||||||
icon_url_override: result.icon?.override,
|
|
||||||
icon_color: result.icon?.color,
|
|
||||||
time_updated: result.time.updated,
|
|
||||||
time_initialized: result.time.initialized,
|
|
||||||
sandboxes: result.sandboxes,
|
|
||||||
commands: result.commands,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
|
|
||||||
if (projectID !== ProjectV2.ID.global) {
|
if (projectID !== ProjectV2.ID.global) {
|
||||||
yield* db
|
yield* db
|
||||||
.update(SessionTable)
|
.update(SessionTable)
|
||||||
.set({ project_id: projectID })
|
.set({ project_id: projectID })
|
||||||
.where(and(eq(SessionTable.project_id, ProjectV2.ID.global), eq(SessionTable.directory, data.directory)))
|
.where(and(eq(SessionTable.project_id, ProjectV2.ID.global), eq(SessionTable.directory, data.directory)))
|
||||||
.run()
|
.run()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* emitUpdated(result)
|
yield* emitUpdated(result)
|
||||||
|
|
@ -362,19 +378,19 @@ export const layer = Layer.effect(
|
||||||
|
|
||||||
const update = Effect.fn("Project.update")(function* (input: UpdateInput) {
|
const update = Effect.fn("Project.update")(function* (input: UpdateInput) {
|
||||||
const result = yield* db
|
const result = yield* db
|
||||||
.update(ProjectTable)
|
.update(ProjectTable)
|
||||||
.set({
|
.set({
|
||||||
name: input.name,
|
name: input.name,
|
||||||
icon_url: input.icon?.url,
|
icon_url: input.icon?.url,
|
||||||
icon_url_override: input.icon?.override,
|
icon_url_override: input.icon?.override,
|
||||||
icon_color: input.icon?.color,
|
icon_color: input.icon?.color,
|
||||||
commands: input.commands,
|
commands: input.commands,
|
||||||
time_updated: Date.now(),
|
time_updated: Date.now(),
|
||||||
})
|
})
|
||||||
.where(eq(ProjectTable.id, input.projectID))
|
.where(eq(ProjectTable.id, input.projectID))
|
||||||
.returning()
|
.returning()
|
||||||
.get()
|
.get()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
if (!result) return yield* new NotFoundError({ projectID: input.projectID })
|
if (!result) return yield* new NotFoundError({ projectID: input.projectID })
|
||||||
const data = fromRow(result)
|
const data = fromRow(result)
|
||||||
yield* emitUpdated(data)
|
yield* emitUpdated(data)
|
||||||
|
|
@ -393,13 +409,19 @@ export const layer = Layer.effect(
|
||||||
})
|
})
|
||||||
|
|
||||||
const setInitialized = Effect.fn("Project.setInitialized")(function* (id: ProjectV2.ID) {
|
const setInitialized = Effect.fn("Project.setInitialized")(function* (id: ProjectV2.ID) {
|
||||||
yield* db.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run().pipe(Effect.orDie)
|
yield* db
|
||||||
|
.update(ProjectTable)
|
||||||
|
.set({ time_initialized: Date.now() })
|
||||||
|
.where(eq(ProjectTable.id, id))
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
})
|
})
|
||||||
|
|
||||||
const initState = yield* InstanceState.make(
|
const initState = yield* InstanceState.make(
|
||||||
Effect.fn("Project.initState")(function* (ctx) {
|
Effect.fn("Project.initState")(function* (ctx) {
|
||||||
const unsubscribe = yield* events.listen((event) => {
|
const unsubscribe = yield* events.listen((event) => {
|
||||||
if (event.type !== Command.Event.Executed.type || event.location?.directory !== ctx.directory) return Effect.void
|
if (event.type !== Command.Event.Executed.type || event.location?.directory !== ctx.directory)
|
||||||
|
return Effect.void
|
||||||
const data = event.data as EventV2.Data<typeof Command.Event.Executed>
|
const data = event.data as EventV2.Data<typeof Command.Event.Executed>
|
||||||
return data.name === Command.Default.INIT ? setInitialized(ctx.project.id) : Effect.void
|
return data.name === Command.Default.INIT ? setInitialized(ctx.project.id) : Effect.void
|
||||||
})
|
})
|
||||||
|
|
@ -432,12 +454,12 @@ export const layer = Layer.effect(
|
||||||
const sboxes = [...row.sandboxes]
|
const sboxes = [...row.sandboxes]
|
||||||
if (!sboxes.includes(directory)) sboxes.push(directory)
|
if (!sboxes.includes(directory)) sboxes.push(directory)
|
||||||
const result = yield* db
|
const result = yield* db
|
||||||
.update(ProjectTable)
|
.update(ProjectTable)
|
||||||
.set({ sandboxes: sboxes, time_updated: Date.now() })
|
.set({ sandboxes: sboxes, time_updated: Date.now() })
|
||||||
.where(eq(ProjectTable.id, id))
|
.where(eq(ProjectTable.id, id))
|
||||||
.returning()
|
.returning()
|
||||||
.get()
|
.get()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
if (!result) throw new Error(`Project not found: ${id}`)
|
if (!result) throw new Error(`Project not found: ${id}`)
|
||||||
yield* emitUpdated(fromRow(result))
|
yield* emitUpdated(fromRow(result))
|
||||||
})
|
})
|
||||||
|
|
@ -447,12 +469,12 @@ export const layer = Layer.effect(
|
||||||
if (!row) throw new Error(`Project not found: ${id}`)
|
if (!row) throw new Error(`Project not found: ${id}`)
|
||||||
const sboxes = row.sandboxes.filter((s) => s !== directory)
|
const sboxes = row.sandboxes.filter((s) => s !== directory)
|
||||||
const result = yield* db
|
const result = yield* db
|
||||||
.update(ProjectTable)
|
.update(ProjectTable)
|
||||||
.set({ sandboxes: sboxes, time_updated: Date.now() })
|
.set({ sandboxes: sboxes, time_updated: Date.now() })
|
||||||
.where(eq(ProjectTable.id, id))
|
.where(eq(ProjectTable.id, id))
|
||||||
.returning()
|
.returning()
|
||||||
.get()
|
.get()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
if (!result) throw new Error(`Project not found: ${id}`)
|
if (!result) throw new Error(`Project not found: ${id}`)
|
||||||
yield* emitUpdated(fromRow(result))
|
yield* emitUpdated(fromRow(result))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -328,7 +328,8 @@ export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Serv
|
||||||
log.info("initialized", { branch: value.current, default_branch: value.root?.name })
|
log.info("initialized", { branch: value.current, default_branch: value.root?.name })
|
||||||
|
|
||||||
const unsubscribe = yield* events.listen((event) => {
|
const unsubscribe = yield* events.listen((event) => {
|
||||||
if (event.type !== FileWatcher.Event.Updated.type || event.location?.directory !== ctx.directory) return Effect.void
|
if (event.type !== FileWatcher.Event.Updated.type || event.location?.directory !== ctx.directory)
|
||||||
|
return Effect.void
|
||||||
const data = event.data as EventV2.Data<typeof FileWatcher.Event.Updated>
|
const data = event.data as EventV2.Data<typeof FileWatcher.Event.Updated>
|
||||||
if (!data.file.endsWith("HEAD")) return Effect.void
|
if (!data.file.endsWith("HEAD")) return Effect.void
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
|
|
@ -429,9 +430,6 @@ export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Serv
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(Layer.provide(Git.defaultLayer), Layer.provide(EventV2Bridge.defaultLayer))
|
||||||
Layer.provide(Git.defaultLayer),
|
|
||||||
Layer.provide(EventV2Bridge.defaultLayer),
|
|
||||||
)
|
|
||||||
|
|
||||||
export * as Vcs from "./vcs"
|
export * as Vcs from "./vcs"
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,9 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const callback = Effect.fn("ProviderAuth.callback")(function* (input: { providerID: ProviderV2.ID } & CallbackInput) {
|
const callback = Effect.fn("ProviderAuth.callback")(function* (
|
||||||
|
input: { providerID: ProviderV2.ID } & CallbackInput,
|
||||||
|
) {
|
||||||
const pending = (yield* InstanceState.get(state)).pending
|
const pending = (yield* InstanceState.get(state)).pending
|
||||||
const match = pending.get(input.providerID)
|
const match = pending.get(input.providerID)
|
||||||
if (!match) return yield* new OauthMissing({ providerID: input.providerID })
|
if (!match) return yield* new OauthMissing({ providerID: input.providerID })
|
||||||
|
|
|
||||||
|
|
@ -1024,14 +1024,20 @@ export type Error = ModelNotFoundError | InitError | NoProvidersError | NoModels
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly list: () => Effect.Effect<Record<ProviderV2.ID, Info>>
|
readonly list: () => Effect.Effect<Record<ProviderV2.ID, Info>>
|
||||||
readonly getProvider: (providerID: ProviderV2.ID) => Effect.Effect<Info>
|
readonly getProvider: (providerID: ProviderV2.ID) => Effect.Effect<Info>
|
||||||
readonly getModel: (providerID: ProviderV2.ID, modelID: ProviderV2.ModelID) => Effect.Effect<Model, ModelNotFoundError>
|
readonly getModel: (
|
||||||
|
providerID: ProviderV2.ID,
|
||||||
|
modelID: ProviderV2.ModelID,
|
||||||
|
) => Effect.Effect<Model, ModelNotFoundError>
|
||||||
readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3, ModelNotFoundError>
|
readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3, ModelNotFoundError>
|
||||||
readonly closest: (
|
readonly closest: (
|
||||||
providerID: ProviderV2.ID,
|
providerID: ProviderV2.ID,
|
||||||
query: string[],
|
query: string[],
|
||||||
) => Effect.Effect<{ providerID: ProviderV2.ID; modelID: string } | undefined>
|
) => Effect.Effect<{ providerID: ProviderV2.ID; modelID: string } | undefined>
|
||||||
readonly getSmallModel: (providerID: ProviderV2.ID) => Effect.Effect<Model | undefined>
|
readonly getSmallModel: (providerID: ProviderV2.ID) => Effect.Effect<Model | undefined>
|
||||||
readonly defaultModel: () => Effect.Effect<{ providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }, DefaultModelError>
|
readonly defaultModel: () => Effect.Effect<
|
||||||
|
{ providerID: ProviderV2.ID; modelID: ProviderV2.ModelID },
|
||||||
|
DefaultModelError
|
||||||
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
export function initProjectors() {
|
export function initProjectors() {}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ export const controlHandlers = HttpApiBuilder.group(RootHttpApi, "control", (han
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: { params: { providerID: ProviderV2.ID } }) {
|
const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: {
|
||||||
|
params: { providerID: ProviderV2.ID }
|
||||||
|
}) {
|
||||||
yield* auth.remove(ctx.params.providerID).pipe(Effect.orDie)
|
yield* auth.remove(ctx.params.providerID).pipe(Effect.orDie)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,10 @@ function eventResponse(events: EventV2.Interface) {
|
||||||
Stream.map((event) => ({ id: event.id, type: event.type, properties: event.data })),
|
Stream.map((event) => ({ id: event.id, type: event.type, properties: event.data })),
|
||||||
)
|
)
|
||||||
const disposed = Stream.callback<{ id: string; type: string; properties: unknown }>((queue) => {
|
const disposed = Stream.callback<{ id: string; type: string; properties: unknown }>((queue) => {
|
||||||
const listener = (event: { directory?: string; payload: { id?: string; type?: string; properties?: unknown } }) => {
|
const listener = (event: {
|
||||||
|
directory?: string
|
||||||
|
payload: { id?: string; type?: string; properties?: unknown }
|
||||||
|
}) => {
|
||||||
if (event.directory !== instance.directory || event.payload.type !== "server.instance.disposed") return
|
if (event.directory !== instance.directory || event.payload.type !== "server.instance.disposed") return
|
||||||
Queue.offerUnsafe(queue, {
|
Queue.offerUnsafe(queue, {
|
||||||
id: event.payload.id ?? eventID(),
|
id: event.payload.id ?? eventID(),
|
||||||
|
|
@ -56,7 +59,10 @@ function eventResponse(events: EventV2.Interface) {
|
||||||
() => Effect.sync(() => GlobalBus.off("event", listener)),
|
() => Effect.sync(() => GlobalBus.off("event", listener)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
const output = stream.pipe(Stream.merge(disposed, { haltStrategy: "left" }), Stream.takeUntil((event) => event.type === "server.instance.disposed"))
|
const output = stream.pipe(
|
||||||
|
Stream.merge(disposed, { haltStrategy: "left" }),
|
||||||
|
Stream.takeUntil((event) => event.type === "server.instance.disposed"),
|
||||||
|
)
|
||||||
const heartbeat = Stream.tick("10 seconds").pipe(
|
const heartbeat = Stream.tick("10 seconds").pipe(
|
||||||
Stream.drop(1),
|
Stream.drop(1),
|
||||||
Stream.map(() => ({ id: eventID(), type: "server.heartbeat", properties: {} })),
|
Stream.map(() => ({ id: eventID(), type: "server.heartbeat", properties: {} })),
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,8 @@ export const tuiHandlers = HttpApiBuilder.group(InstanceHttpApi, "tui", (handler
|
||||||
yield* events.publish(TuiEvent.PromptAppend, ctx.payload.properties)
|
yield* events.publish(TuiEvent.PromptAppend, ctx.payload.properties)
|
||||||
if (ctx.payload.type === TuiEvent.CommandExecute.type)
|
if (ctx.payload.type === TuiEvent.CommandExecute.type)
|
||||||
yield* events.publish(TuiEvent.CommandExecute, ctx.payload.properties)
|
yield* events.publish(TuiEvent.CommandExecute, ctx.payload.properties)
|
||||||
if (ctx.payload.type === TuiEvent.ToastShow.type) yield* events.publish(TuiEvent.ToastShow, ctx.payload.properties)
|
if (ctx.payload.type === TuiEvent.ToastShow.type)
|
||||||
|
yield* events.publish(TuiEvent.ToastShow, ctx.payload.properties)
|
||||||
if (ctx.payload.type === TuiEvent.SessionSelect.type)
|
if (ctx.payload.type === TuiEvent.SessionSelect.type)
|
||||||
yield* events.publish(TuiEvent.SessionSelect, ctx.payload.properties)
|
yield* events.publish(TuiEvent.SessionSelect, ctx.payload.properties)
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -353,7 +353,9 @@ export const layer = Layer.effect(
|
||||||
throw new Error(`Compaction parent must be a user message: ${input.parentID}`)
|
throw new Error(`Compaction parent must be a user message: ${input.parentID}`)
|
||||||
}
|
}
|
||||||
const userMessage = parent.info
|
const userMessage = parent.info
|
||||||
const compactionPart = parent.parts.find((part): part is SessionLegacy.CompactionPart => part.type === "compaction")
|
const compactionPart = parent.parts.find(
|
||||||
|
(part): part is SessionLegacy.CompactionPart => part.type === "compaction",
|
||||||
|
)
|
||||||
|
|
||||||
let messages = input.messages
|
let messages = input.messages
|
||||||
let replay:
|
let replay:
|
||||||
|
|
|
||||||
|
|
@ -168,13 +168,15 @@ const live: Layer.Layer<
|
||||||
const id = PermissionID.ascending()
|
const id = PermissionID.ascending()
|
||||||
let unsub: EventV2.Unsubscribe | undefined
|
let unsub: EventV2.Unsubscribe | undefined
|
||||||
try {
|
try {
|
||||||
unsub = await bridge.promise(events.listen((event) => {
|
unsub = await bridge.promise(
|
||||||
if (event.type !== Permission.Event.Replied.type) return Effect.void
|
events.listen((event) => {
|
||||||
const data = event.data as EventV2.Data<typeof Permission.Event.Replied>
|
if (event.type !== Permission.Event.Replied.type) return Effect.void
|
||||||
if (data.requestID !== id) return Effect.void
|
const data = event.data as EventV2.Data<typeof Permission.Event.Replied>
|
||||||
void data.reply
|
if (data.requestID !== id) return Effect.void
|
||||||
return Effect.void
|
void data.reply
|
||||||
}))
|
return Effect.void
|
||||||
|
}),
|
||||||
|
)
|
||||||
const toolPatterns = approvalTools.map((t: { name: string; args: string }) => {
|
const toolPatterns = approvalTools.map((t: { name: string; args: string }) => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(t.args) as Record<string, unknown>
|
const parsed = JSON.parse(t.args) as Record<string, unknown>
|
||||||
|
|
|
||||||
|
|
@ -503,14 +503,14 @@ export function stream(sessionID: SessionID) {
|
||||||
export function parts(messageID: MessageID) {
|
export function parts(messageID: MessageID) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
const rows = yield* db
|
const rows = yield* db
|
||||||
.select()
|
.select()
|
||||||
.from(PartTable)
|
.from(PartTable)
|
||||||
.where(eq(PartTable.message_id, messageID))
|
.where(eq(PartTable.message_id, messageID))
|
||||||
.orderBy(PartTable.id)
|
.orderBy(PartTable.id)
|
||||||
.all()
|
.all()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
return rows.map(part)
|
return rows.map(part)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1502,11 +1502,11 @@ export const layer = Layer.effect(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const loop: (input: LoopInput) => Effect.Effect<SessionLegacy.WithParts> = Effect.fn("SessionPrompt.loop")(function* (
|
const loop: (input: LoopInput) => Effect.Effect<SessionLegacy.WithParts> = Effect.fn("SessionPrompt.loop")(
|
||||||
input: LoopInput,
|
function* (input: LoopInput) {
|
||||||
) {
|
return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID))
|
||||||
return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID))
|
},
|
||||||
})
|
)
|
||||||
|
|
||||||
const shell: (input: ShellInput) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError> = Effect.fn(
|
const shell: (input: ShellInput) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError> = Effect.fn(
|
||||||
"SessionPrompt.shell",
|
"SessionPrompt.shell",
|
||||||
|
|
|
||||||
|
|
@ -536,11 +536,7 @@ export type Patch = Omit<Partial<Info>, "time" | "share" | "summary" | "revert"
|
||||||
export const layer: Layer.Layer<
|
export const layer: Layer.Layer<
|
||||||
Service,
|
Service,
|
||||||
never,
|
never,
|
||||||
| BackgroundJob.Service
|
BackgroundJob.Service | Storage.Service | RuntimeFlags.Service | Database.Service | EventV2Bridge.Service
|
||||||
| Storage.Service
|
|
||||||
| RuntimeFlags.Service
|
|
||||||
| Database.Service
|
|
||||||
| EventV2Bridge.Service
|
|
||||||
> = Layer.effect(
|
> = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,9 @@ export const layer = Layer.effect(
|
||||||
const storage = yield* Storage.Service
|
const storage = yield* Storage.Service
|
||||||
const events = yield* EventV2Bridge.Service
|
const events = yield* EventV2Bridge.Service
|
||||||
|
|
||||||
const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: { messages: SessionLegacy.WithParts[] }) {
|
const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: {
|
||||||
|
messages: SessionLegacy.WithParts[]
|
||||||
|
}) {
|
||||||
let from: string | undefined
|
let from: string | undefined
|
||||||
let to: string | undefined
|
let to: string | undefined
|
||||||
for (const item of input.messages) {
|
for (const item of input.messages) {
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,9 @@ export const layer = Layer.effect(
|
||||||
events.listen((event) => {
|
events.listen((event) => {
|
||||||
if (event.type !== def.type || event.location?.directory !== _ctx.directory) return Effect.void
|
if (event.type !== def.type || event.location?.directory !== _ctx.directory) return Effect.void
|
||||||
return fn(event.data as EventV2.Data<D>).pipe(
|
return fn(event.data as EventV2.Data<D>).pipe(
|
||||||
Effect.catchCause((cause) => Effect.sync(() => log.error("share subscriber failed", { type: def.type, cause }))),
|
Effect.catchCause((cause) =>
|
||||||
|
Effect.sync(() => log.error("share subscriber failed", { type: def.type, cause })),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,11 @@ const discoverSkills = Effect.fnUntraced(function* (
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadSkills = Effect.fnUntraced(function* (state: State, discovered: DiscoveryState, events: EventV2Bridge.Service["Service"]) {
|
const loadSkills = Effect.fnUntraced(function* (
|
||||||
|
state: State,
|
||||||
|
discovered: DiscoveryState,
|
||||||
|
events: EventV2Bridge.Service["Service"],
|
||||||
|
) {
|
||||||
yield* Effect.forEach(discovered.matches, (match) => add(state, match, events), {
|
yield* Effect.forEach(discovered.matches, (match) => add(state, match, events), {
|
||||||
concurrency: "unbounded",
|
concurrency: "unbounded",
|
||||||
discard: true,
|
discard: true,
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,11 @@ export interface Interface {
|
||||||
readonly ids: () => Effect.Effect<string[]>
|
readonly ids: () => Effect.Effect<string[]>
|
||||||
readonly all: () => Effect.Effect<Tool.Def[]>
|
readonly all: () => Effect.Effect<Tool.Def[]>
|
||||||
readonly named: () => Effect.Effect<{ task: TaskDef; read: ReadDef }>
|
readonly named: () => Effect.Effect<{ task: TaskDef; read: ReadDef }>
|
||||||
readonly tools: (model: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID; agent: Agent.Info }) => Effect.Effect<Tool.Def[]>
|
readonly tools: (model: {
|
||||||
|
providerID: ProviderV2.ID
|
||||||
|
modelID: ProviderV2.ModelID
|
||||||
|
agent: Agent.Info
|
||||||
|
}) => Effect.Effect<Tool.Def[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ToolRegistry") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/ToolRegistry") {}
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,13 @@ type GitResult = { code: number; text: string; stderr: string }
|
||||||
export const layer: Layer.Layer<
|
export const layer: Layer.Layer<
|
||||||
Service,
|
Service,
|
||||||
never,
|
never,
|
||||||
AppFileSystem.Service | Path.Path | AppProcess.Service | Git.Service | Project.Service | InstanceStore.Service | Database.Service
|
| AppFileSystem.Service
|
||||||
|
| Path.Path
|
||||||
|
| AppProcess.Service
|
||||||
|
| Git.Service
|
||||||
|
| Project.Service
|
||||||
|
| InstanceStore.Service
|
||||||
|
| Database.Service
|
||||||
> = Layer.effect(
|
> = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -484,7 +490,12 @@ export const layer: Layer.Layer<
|
||||||
directory: string,
|
directory: string,
|
||||||
input: { projectID: ProjectV2.ID; extra?: string },
|
input: { projectID: ProjectV2.ID; extra?: string },
|
||||||
) {
|
) {
|
||||||
const row = yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, input.projectID)).get().pipe(Effect.orDie)
|
const row = yield* db
|
||||||
|
.select()
|
||||||
|
.from(ProjectTable)
|
||||||
|
.where(eq(ProjectTable.id, input.projectID))
|
||||||
|
.get()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
const project = row ? Project.fromRow(row) : undefined
|
const project = row ? Project.fromRow(row) : undefined
|
||||||
const startup = project?.commands?.start?.trim() ?? ""
|
const startup = project?.commands?.start?.trim() ?? ""
|
||||||
const ok = yield* runStartScript(directory, startup, "project")
|
const ok = yield* runStartScript(directory, startup, "project")
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,11 @@ function createReasoningPart(text: string): SessionLegacy.Part {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): SessionLegacy.Part {
|
function createToolPart(
|
||||||
|
tool: string,
|
||||||
|
title: string,
|
||||||
|
status: "completed" | "running" = "completed",
|
||||||
|
): SessionLegacy.Part {
|
||||||
if (status === "completed") {
|
if (status === "completed") {
|
||||||
return {
|
return {
|
||||||
id: PartID.ascending(),
|
id: PartID.ascending(),
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,9 @@ describe("Format", () => {
|
||||||
{ config: { formatter: false } },
|
{ config: { formatter: false } },
|
||||||
)
|
)
|
||||||
|
|
||||||
testEffect(Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer, testInstanceStoreLayer)).live("status() initializes formatter state per directory", () =>
|
testEffect(
|
||||||
|
Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer, testInstanceStoreLayer),
|
||||||
|
).live("status() initializes formatter state per directory", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const a = yield* provideTmpdirInstance(() => Format.use.status(), {
|
const a = yield* provideTmpdirInstance(() => Format.use.status(), {
|
||||||
config: { formatter: false },
|
config: { formatter: false },
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,11 @@ const lspLayer = (flags: Parameters<typeof RuntimeFlags.layer>[0] = {}) =>
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(lspLayer(), CrossSpawnSpawner.defaultLayer))
|
const it = testEffect(Layer.mergeAll(lspLayer(), CrossSpawnSpawner.defaultLayer))
|
||||||
const experimentalTyIt = testEffect(
|
const experimentalTyIt = testEffect(
|
||||||
Layer.mergeAll(
|
Layer.mergeAll(lspLayer({ experimentalLspTy: true }), CrossSpawnSpawner.defaultLayer),
|
||||||
lspLayer({ experimentalLspTy: true }),
|
|
||||||
CrossSpawnSpawner.defaultLayer,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
const fakeServerPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
|
const fakeServerPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
|
||||||
const disabledDownloadIt = testEffect(
|
const disabledDownloadIt = testEffect(
|
||||||
Layer.mergeAll(
|
Layer.mergeAll(lspLayer({ disableLspDownload: true }), CrossSpawnSpawner.defaultLayer),
|
||||||
lspLayer({ disableLspDownload: true }),
|
|
||||||
CrossSpawnSpawner.defaultLayer,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
describe("lsp.spawn", () => {
|
describe("lsp.spawn", () => {
|
||||||
|
|
@ -37,27 +31,49 @@ describe("lsp.spawn", () => {
|
||||||
"does not spawn builtin LSP for files outside instance",
|
"does not spawn builtin LSP for files outside instance",
|
||||||
() =>
|
() =>
|
||||||
LSP.Service.use((lsp) =>
|
LSP.Service.use((lsp) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
yield* lsp.touchFile(path.join(dir, "..", "outside.ts"))
|
yield* lsp.touchFile(path.join(dir, "..", "outside.ts"))
|
||||||
yield* lsp.hover({
|
yield* lsp.hover({
|
||||||
file: path.join(dir, "..", "hover.ts"),
|
file: path.join(dir, "..", "hover.ts"),
|
||||||
line: 0,
|
line: 0,
|
||||||
character: 0,
|
character: 0,
|
||||||
})
|
})
|
||||||
expect(spy).toHaveBeenCalledTimes(0)
|
expect(spy).toHaveBeenCalledTimes(0)
|
||||||
} finally {
|
} finally {
|
||||||
spy.mockRestore()
|
spy.mockRestore()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ config: { lsp: true } },
|
{ config: { lsp: true } },
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("does not spawn builtin LSP for files inside instance when LSP is unset", () =>
|
it.instance("does not spawn builtin LSP for files inside instance when LSP is unset", () =>
|
||||||
|
LSP.Service.use((lsp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const dir = (yield* TestInstance).directory
|
||||||
|
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
||||||
|
|
||||||
|
try {
|
||||||
|
yield* lsp.hover({
|
||||||
|
file: path.join(dir, "src", "inside.ts"),
|
||||||
|
line: 0,
|
||||||
|
character: 0,
|
||||||
|
})
|
||||||
|
expect(spy).toHaveBeenCalledTimes(0)
|
||||||
|
} finally {
|
||||||
|
spy.mockRestore()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.instance(
|
||||||
|
"would spawn builtin LSP for files inside instance when lsp is true",
|
||||||
|
() =>
|
||||||
LSP.Service.use((lsp) =>
|
LSP.Service.use((lsp) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
|
|
@ -69,34 +85,12 @@ describe("lsp.spawn", () => {
|
||||||
line: 0,
|
line: 0,
|
||||||
character: 0,
|
character: 0,
|
||||||
})
|
})
|
||||||
expect(spy).toHaveBeenCalledTimes(0)
|
expect(spy).toHaveBeenCalledTimes(1)
|
||||||
} finally {
|
} finally {
|
||||||
spy.mockRestore()
|
spy.mockRestore()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
|
|
||||||
it.instance(
|
|
||||||
"would spawn builtin LSP for files inside instance when lsp is true",
|
|
||||||
() =>
|
|
||||||
LSP.Service.use((lsp) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const dir = (yield* TestInstance).directory
|
|
||||||
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
|
||||||
|
|
||||||
try {
|
|
||||||
yield* lsp.hover({
|
|
||||||
file: path.join(dir, "src", "inside.ts"),
|
|
||||||
line: 0,
|
|
||||||
character: 0,
|
|
||||||
})
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
|
||||||
} finally {
|
|
||||||
spy.mockRestore()
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
{ config: { lsp: true } },
|
{ config: { lsp: true } },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -104,21 +98,21 @@ describe("lsp.spawn", () => {
|
||||||
"publishes lsp.updated after custom LSP initialization",
|
"publishes lsp.updated after custom LSP initialization",
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
const lsp = yield* LSP.Service
|
const lsp = yield* LSP.Service
|
||||||
const updated = yield* Deferred.make<void>()
|
const updated = yield* Deferred.make<void>()
|
||||||
const events = yield* EventV2Bridge.Service
|
const events = yield* EventV2Bridge.Service
|
||||||
const unsubscribe = yield* events.listen((event) => {
|
const unsubscribe = yield* events.listen((event) => {
|
||||||
if (event.type === LSP.Event.Updated.type) Deferred.doneUnsafe(updated, Effect.void)
|
if (event.type === LSP.Event.Updated.type) Deferred.doneUnsafe(updated, Effect.void)
|
||||||
return Effect.void
|
return Effect.void
|
||||||
})
|
})
|
||||||
yield* Effect.addFinalizer(() => unsubscribe)
|
yield* Effect.addFinalizer(() => unsubscribe)
|
||||||
|
|
||||||
const file = path.join(dir, "sample.repro")
|
const file = path.join(dir, "sample.repro")
|
||||||
yield* Effect.promise(() => Bun.write(file, "sample\n"))
|
yield* Effect.promise(() => Bun.write(file, "sample\n"))
|
||||||
yield* lsp.touchFile(file)
|
yield* lsp.touchFile(file)
|
||||||
yield* awaitWithTimeout(Deferred.await(updated), "lsp.updated event was not published")
|
yield* awaitWithTimeout(Deferred.await(updated), "lsp.updated event was not published")
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
config: {
|
config: {
|
||||||
lsp: {
|
lsp: {
|
||||||
|
|
@ -135,22 +129,22 @@ describe("lsp.spawn", () => {
|
||||||
"would spawn builtin LSP for files inside instance when config object is provided",
|
"would spawn builtin LSP for files inside instance when config object is provided",
|
||||||
() =>
|
() =>
|
||||||
LSP.Service.use((lsp) =>
|
LSP.Service.use((lsp) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
yield* lsp.hover({
|
yield* lsp.hover({
|
||||||
file: path.join(dir, "src", "inside.ts"),
|
file: path.join(dir, "src", "inside.ts"),
|
||||||
line: 0,
|
line: 0,
|
||||||
character: 0,
|
character: 0,
|
||||||
})
|
})
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
expect(spy).toHaveBeenCalledTimes(1)
|
||||||
} finally {
|
} finally {
|
||||||
spy.mockRestore()
|
spy.mockRestore()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
config: {
|
config: {
|
||||||
lsp: {
|
lsp: {
|
||||||
|
|
@ -164,25 +158,25 @@ describe("lsp.spawn", () => {
|
||||||
"uses pyright instead of ty by default",
|
"uses pyright instead of ty by default",
|
||||||
() =>
|
() =>
|
||||||
LSP.Service.use((lsp) =>
|
LSP.Service.use((lsp) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined)
|
const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined)
|
||||||
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
|
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
yield* lsp.hover({
|
yield* lsp.hover({
|
||||||
file: path.join(dir, "src", "inside.py"),
|
file: path.join(dir, "src", "inside.py"),
|
||||||
line: 0,
|
line: 0,
|
||||||
character: 0,
|
character: 0,
|
||||||
})
|
})
|
||||||
expect(ty).toHaveBeenCalledTimes(0)
|
expect(ty).toHaveBeenCalledTimes(0)
|
||||||
expect(pyright).toHaveBeenCalledTimes(1)
|
expect(pyright).toHaveBeenCalledTimes(1)
|
||||||
} finally {
|
} finally {
|
||||||
ty.mockRestore()
|
ty.mockRestore()
|
||||||
pyright.mockRestore()
|
pyright.mockRestore()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ config: { lsp: true } },
|
{ config: { lsp: true } },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -190,25 +184,25 @@ describe("lsp.spawn", () => {
|
||||||
"uses ty instead of pyright when experimentalLspTy is enabled",
|
"uses ty instead of pyright when experimentalLspTy is enabled",
|
||||||
() =>
|
() =>
|
||||||
LSP.Service.use((lsp) =>
|
LSP.Service.use((lsp) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined)
|
const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined)
|
||||||
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
|
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
yield* lsp.hover({
|
yield* lsp.hover({
|
||||||
file: path.join(dir, "src", "inside.py"),
|
file: path.join(dir, "src", "inside.py"),
|
||||||
line: 0,
|
line: 0,
|
||||||
character: 0,
|
character: 0,
|
||||||
})
|
})
|
||||||
expect(ty).toHaveBeenCalledTimes(1)
|
expect(ty).toHaveBeenCalledTimes(1)
|
||||||
expect(pyright).toHaveBeenCalledTimes(0)
|
expect(pyright).toHaveBeenCalledTimes(0)
|
||||||
} finally {
|
} finally {
|
||||||
ty.mockRestore()
|
ty.mockRestore()
|
||||||
pyright.mockRestore()
|
pyright.mockRestore()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ config: { lsp: true } },
|
{ config: { lsp: true } },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -216,23 +210,23 @@ describe("lsp.spawn", () => {
|
||||||
"passes disableLspDownload to builtin LSP spawn",
|
"passes disableLspDownload to builtin LSP spawn",
|
||||||
() =>
|
() =>
|
||||||
LSP.Service.use((lsp) =>
|
LSP.Service.use((lsp) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
|
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
yield* lsp.hover({
|
yield* lsp.hover({
|
||||||
file: path.join(dir, "src", "inside.py"),
|
file: path.join(dir, "src", "inside.py"),
|
||||||
line: 0,
|
line: 0,
|
||||||
character: 0,
|
character: 0,
|
||||||
})
|
})
|
||||||
expect(pyright).toHaveBeenCalledTimes(1)
|
expect(pyright).toHaveBeenCalledTimes(1)
|
||||||
expect(pyright.mock.calls[0]?.[2]).toMatchObject({ disableLspDownload: true })
|
expect(pyright.mock.calls[0]?.[2]).toMatchObject({ disableLspDownload: true })
|
||||||
} finally {
|
} finally {
|
||||||
pyright.mockRestore()
|
pyright.mockRestore()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ config: { lsp: true } },
|
{ config: { lsp: true } },
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,12 @@ describe("LSP service lifecycle", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("hasClients() returns false for .ts files in instance when LSP is unset", () =>
|
it.instance("hasClients() returns false for .ts files in instance when LSP is unset", () =>
|
||||||
LSP.Service.use((lsp) =>
|
LSP.Service.use((lsp) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const result = yield* lsp.hasClients(path.join((yield* TestInstance).directory, "test.ts"))
|
const result = yield* lsp.hasClients(path.join((yield* TestInstance).directory, "test.ts"))
|
||||||
expect(result).toBe(false)
|
expect(result).toBe(false)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
|
|
|
||||||
|
|
@ -657,7 +657,8 @@ it.instance(
|
||||||
const events = yield* EventV2Bridge.Service
|
const events = yield* EventV2Bridge.Service
|
||||||
const seen = yield* Deferred.make<Permission.Request>()
|
const seen = yield* Deferred.make<Permission.Request>()
|
||||||
const unsub = yield* events.listen((event) => {
|
const unsub = yield* events.listen((event) => {
|
||||||
if (event.type === Permission.Event.Asked.type) Deferred.doneUnsafe(seen, Effect.succeed(event.data as Permission.Request))
|
if (event.type === Permission.Event.Asked.type)
|
||||||
|
Deferred.doneUnsafe(seen, Effect.succeed(event.data as Permission.Request))
|
||||||
return Effect.void
|
return Effect.void
|
||||||
})
|
})
|
||||||
yield* Effect.addFinalizer(() => unsub)
|
yield* Effect.addFinalizer(() => unsub)
|
||||||
|
|
@ -932,7 +933,10 @@ it.instance(
|
||||||
|
|
||||||
const unsub = yield* events.listen((event) => {
|
const unsub = yield* events.listen((event) => {
|
||||||
if (event.type === Permission.Event.Replied.type)
|
if (event.type === Permission.Event.Replied.type)
|
||||||
Deferred.doneUnsafe(seen, Effect.succeed(event.data as { sessionID: SessionID; requestID: PermissionID; reply: Permission.Reply }))
|
Deferred.doneUnsafe(
|
||||||
|
seen,
|
||||||
|
Effect.succeed(event.data as { sessionID: SessionID; requestID: PermissionID; reply: Permission.Reply }),
|
||||||
|
)
|
||||||
return Effect.void
|
return Effect.void
|
||||||
})
|
})
|
||||||
yield* Effect.addFinalizer(() => unsub)
|
yield* Effect.addFinalizer(() => unsub)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ afterEach(async () => {
|
||||||
await disposeAllInstances()
|
await disposeAllInstances()
|
||||||
})
|
})
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, AppFileSystem.defaultLayer, testInstanceStoreLayer))
|
const it = testEffect(
|
||||||
|
Layer.mergeAll(CrossSpawnSpawner.defaultLayer, AppFileSystem.defaultLayer, testInstanceStoreLayer),
|
||||||
|
)
|
||||||
|
|
||||||
function withTmp<T, A, E, R>(
|
function withTmp<T, A, E, R>(
|
||||||
init: (dir: string) => Promise<T>,
|
init: (dir: string) => Promise<T>,
|
||||||
|
|
|
||||||
|
|
@ -64,74 +64,74 @@ afterEach(async () => {
|
||||||
describe("plugin.workspace", () => {
|
describe("plugin.workspace", () => {
|
||||||
it.instance("plugin can install a workspace adapter", () =>
|
it.instance("plugin can install a workspace adapter", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
const type = `plug-${Math.random().toString(36).slice(2)}`
|
const type = `plug-${Math.random().toString(36).slice(2)}`
|
||||||
const file = path.join(dir, "plugin.ts")
|
const file = path.join(dir, "plugin.ts")
|
||||||
const mark = path.join(dir, "created.json")
|
const mark = path.join(dir, "created.json")
|
||||||
const space = path.join(dir, "space")
|
const space = path.join(dir, "space")
|
||||||
yield* Effect.promise(() =>
|
yield* Effect.promise(() =>
|
||||||
Bun.write(
|
Bun.write(
|
||||||
file,
|
file,
|
||||||
[
|
[
|
||||||
"export default async ({ experimental_workspace }) => {",
|
"export default async ({ experimental_workspace }) => {",
|
||||||
` experimental_workspace.register(${JSON.stringify(type)}, {`,
|
` experimental_workspace.register(${JSON.stringify(type)}, {`,
|
||||||
' name: "plug",',
|
' name: "plug",',
|
||||||
' description: "plugin workspace adapter",',
|
' description: "plugin workspace adapter",',
|
||||||
" configure(input) {",
|
" configure(input) {",
|
||||||
` return { ...input, name: "plug", branch: "plug/main", directory: ${JSON.stringify(space)} }`,
|
` return { ...input, name: "plug", branch: "plug/main", directory: ${JSON.stringify(space)} }`,
|
||||||
" },",
|
" },",
|
||||||
" async create(input) {",
|
" async create(input) {",
|
||||||
` await Bun.write(${JSON.stringify(mark)}, JSON.stringify(input))`,
|
` await Bun.write(${JSON.stringify(mark)}, JSON.stringify(input))`,
|
||||||
" },",
|
" },",
|
||||||
" async remove() {},",
|
" async remove() {},",
|
||||||
" target(input) {",
|
" target(input) {",
|
||||||
' return { type: "local", directory: input.directory }',
|
' return { type: "local", directory: input.directory }',
|
||||||
" },",
|
" },",
|
||||||
" })",
|
" })",
|
||||||
" return {}",
|
" return {}",
|
||||||
"}",
|
"}",
|
||||||
"",
|
"",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Effect.promise(() =>
|
||||||
|
Bun.write(
|
||||||
|
path.join(dir, "opencode.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
$schema: "https://opencode.ai/config.json",
|
||||||
|
plugin: [pathToFileURL(file).href],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
|
||||||
yield* Effect.promise(() =>
|
const plugin = yield* Plugin.Service
|
||||||
Bun.write(
|
yield* plugin.init()
|
||||||
path.join(dir, "opencode.json"),
|
const workspace = yield* Workspace.Service
|
||||||
JSON.stringify(
|
const ctx = yield* InstanceState.context
|
||||||
{
|
const info = yield* workspace.create({
|
||||||
$schema: "https://opencode.ai/config.json",
|
type,
|
||||||
plugin: [pathToFileURL(file).href],
|
branch: null,
|
||||||
},
|
extra: { key: "value" },
|
||||||
null,
|
projectID: ctx.project.id,
|
||||||
2,
|
})
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const plugin = yield* Plugin.Service
|
expect(info.type).toBe(type)
|
||||||
yield* plugin.init()
|
expect(info.name).toBe("plug")
|
||||||
const workspace = yield* Workspace.Service
|
expect(info.branch).toBe("plug/main")
|
||||||
const ctx = yield* InstanceState.context
|
expect(info.directory).toBe(space)
|
||||||
const info = yield* workspace.create({
|
expect(info.extra).toEqual({ key: "value" })
|
||||||
type,
|
expect(JSON.parse(yield* Effect.promise(() => Bun.file(mark).text()))).toMatchObject({
|
||||||
branch: null,
|
type,
|
||||||
extra: { key: "value" },
|
name: "plug",
|
||||||
projectID: ctx.project.id,
|
branch: "plug/main",
|
||||||
})
|
directory: space,
|
||||||
|
extra: { key: "value" },
|
||||||
expect(info.type).toBe(type)
|
})
|
||||||
expect(info.name).toBe("plug")
|
|
||||||
expect(info.branch).toBe("plug/main")
|
|
||||||
expect(info.directory).toBe(space)
|
|
||||||
expect(info.extra).toEqual({ key: "value" })
|
|
||||||
expect(JSON.parse(yield* Effect.promise(() => Bun.file(mark).text()))).toMatchObject({
|
|
||||||
type,
|
|
||||||
name: "plug",
|
|
||||||
branch: "plug/main",
|
|
||||||
directory: space,
|
|
||||||
extra: { key: "value" },
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -238,10 +238,25 @@ describe("Project.fromDirectory", () => {
|
||||||
const result = yield* projects.fromDirectory(tmp)
|
const result = yield* projects.fromDirectory(tmp)
|
||||||
|
|
||||||
expect(result.project.id).toBe(remoteID)
|
expect(result.project.id).toBe(remoteID)
|
||||||
expect(yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, rootProject.id)).get().pipe(Effect.orDie)).toBeUndefined()
|
expect(
|
||||||
expect((yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie))?.project_id).toBe(remoteID)
|
yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, rootProject.id)).get().pipe(Effect.orDie),
|
||||||
expect(yield* db.select().from(PermissionTable).where(eq(PermissionTable.project_id, remoteID)).get().pipe(Effect.orDie)).toBeDefined()
|
).toBeUndefined()
|
||||||
expect((yield* db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, workspaceID)).get().pipe(Effect.orDie))?.project_id).toBe(remoteID)
|
expect(
|
||||||
|
(yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie))
|
||||||
|
?.project_id,
|
||||||
|
).toBe(remoteID)
|
||||||
|
expect(
|
||||||
|
yield* db
|
||||||
|
.select()
|
||||||
|
.from(PermissionTable)
|
||||||
|
.where(eq(PermissionTable.project_id, remoteID))
|
||||||
|
.get()
|
||||||
|
.pipe(Effect.orDie),
|
||||||
|
).toBeDefined()
|
||||||
|
expect(
|
||||||
|
(yield* db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, workspaceID)).get().pipe(Effect.orDie))
|
||||||
|
?.project_id,
|
||||||
|
).toBe(remoteID)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,13 @@ import { Deferred, Effect, Layer } from "effect"
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { disposeAllInstances, provideInstance, testInstanceStoreLayer, TestInstance, tmpdirScoped } from "../fixture/fixture"
|
import {
|
||||||
|
disposeAllInstances,
|
||||||
|
provideInstance,
|
||||||
|
testInstanceStoreLayer,
|
||||||
|
TestInstance,
|
||||||
|
tmpdirScoped,
|
||||||
|
} from "../fixture/fixture"
|
||||||
import { EventV2Bridge } from "../../src/event-v2-bridge"
|
import { EventV2Bridge } from "../../src/event-v2-bridge"
|
||||||
import { FileWatcher } from "../../src/file/watcher"
|
import { FileWatcher } from "../../src/file/watcher"
|
||||||
import { Git } from "../../src/git"
|
import { Git } from "../../src/git"
|
||||||
|
|
|
||||||
|
|
@ -16,70 +16,70 @@ describe("Worktree.remove", () => {
|
||||||
"continues when git remove exits non-zero after detaching",
|
"continues when git remove exits non-zero after detaching",
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const root = (yield* TestInstance).directory
|
const root = (yield* TestInstance).directory
|
||||||
const svc = yield* Worktree.Service
|
const svc = yield* Worktree.Service
|
||||||
const name = `remove-regression-${Date.now().toString(36)}`
|
const name = `remove-regression-${Date.now().toString(36)}`
|
||||||
const branch = `opencode/${name}`
|
const branch = `opencode/${name}`
|
||||||
const dir = path.join(root, "..", name)
|
const dir = path.join(root, "..", name)
|
||||||
|
|
||||||
yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
|
yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
|
||||||
yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
|
yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
|
||||||
|
|
||||||
const real = (yield* Effect.promise(() => $`which git`.quiet().text())).trim()
|
const real = (yield* Effect.promise(() => $`which git`.quiet().text())).trim()
|
||||||
expect(real).toBeTruthy()
|
expect(real).toBeTruthy()
|
||||||
|
|
||||||
const bin = path.join(root, "bin")
|
const bin = path.join(root, "bin")
|
||||||
const shim = path.join(bin, "git")
|
const shim = path.join(bin, "git")
|
||||||
yield* Effect.promise(() => fs.mkdir(bin, { recursive: true }))
|
yield* Effect.promise(() => fs.mkdir(bin, { recursive: true }))
|
||||||
yield* Effect.promise(() =>
|
yield* Effect.promise(() =>
|
||||||
Bun.write(
|
Bun.write(
|
||||||
shim,
|
shim,
|
||||||
[
|
[
|
||||||
"#!/bin/bash",
|
"#!/bin/bash",
|
||||||
`REAL_GIT=${JSON.stringify(real)}`,
|
`REAL_GIT=${JSON.stringify(real)}`,
|
||||||
'if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then',
|
'if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then',
|
||||||
' "$REAL_GIT" "$@" >/dev/null 2>&1',
|
' "$REAL_GIT" "$@" >/dev/null 2>&1',
|
||||||
' echo "fatal: failed to remove worktree: Directory not empty" >&2',
|
' echo "fatal: failed to remove worktree: Directory not empty" >&2',
|
||||||
" exit 1",
|
" exit 1",
|
||||||
"fi",
|
"fi",
|
||||||
'exec "$REAL_GIT" "$@"',
|
'exec "$REAL_GIT" "$@"',
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
yield* Effect.promise(() => fs.chmod(shim, 0o755))
|
yield* Effect.promise(() => fs.chmod(shim, 0o755))
|
||||||
|
|
||||||
const prev = yield* Effect.acquireRelease(
|
const prev = yield* Effect.acquireRelease(
|
||||||
|
Effect.sync(() => {
|
||||||
|
const prev = process.env.PATH ?? ""
|
||||||
|
process.env.PATH = `${bin}${path.delimiter}${prev}`
|
||||||
|
return prev
|
||||||
|
}),
|
||||||
|
(prev) =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
const prev = process.env.PATH ?? ""
|
process.env.PATH = prev
|
||||||
process.env.PATH = `${bin}${path.delimiter}${prev}`
|
|
||||||
return prev
|
|
||||||
}),
|
}),
|
||||||
(prev) =>
|
)
|
||||||
Effect.sync(() => {
|
void prev
|
||||||
process.env.PATH = prev
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
void prev
|
|
||||||
|
|
||||||
const ok = yield* svc.remove({ directory: dir })
|
const ok = yield* svc.remove({ directory: dir })
|
||||||
|
|
||||||
expect(ok).toBe(true)
|
expect(ok).toBe(true)
|
||||||
expect(
|
expect(
|
||||||
yield* Effect.promise(() =>
|
yield* Effect.promise(() =>
|
||||||
fs
|
fs
|
||||||
.stat(dir)
|
.stat(dir)
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false),
|
.catch(() => false),
|
||||||
),
|
),
|
||||||
).toBe(false)
|
).toBe(false)
|
||||||
|
|
||||||
const list = yield* Effect.promise(() => $`git worktree list --porcelain`.cwd(root).quiet().text())
|
const list = yield* Effect.promise(() => $`git worktree list --porcelain`.cwd(root).quiet().text())
|
||||||
expect(list).not.toContain(`worktree ${dir}`)
|
expect(list).not.toContain(`worktree ${dir}`)
|
||||||
|
|
||||||
const ref = yield* Effect.promise(() =>
|
const ref = yield* Effect.promise(() =>
|
||||||
$`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
|
$`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
|
||||||
)
|
)
|
||||||
expect(ref.exitCode).not.toBe(0)
|
expect(ref.exitCode).not.toBe(0)
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
@ -88,38 +88,38 @@ describe("Worktree.remove", () => {
|
||||||
"stops fsmonitor before removing a worktree",
|
"stops fsmonitor before removing a worktree",
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const root = (yield* TestInstance).directory
|
const root = (yield* TestInstance).directory
|
||||||
const svc = yield* Worktree.Service
|
const svc = yield* Worktree.Service
|
||||||
const name = `remove-fsmonitor-${Date.now().toString(36)}`
|
const name = `remove-fsmonitor-${Date.now().toString(36)}`
|
||||||
const branch = `opencode/${name}`
|
const branch = `opencode/${name}`
|
||||||
const dir = path.join(root, "..", name)
|
const dir = path.join(root, "..", name)
|
||||||
|
|
||||||
yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
|
yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
|
||||||
yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
|
yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
|
||||||
yield* Effect.promise(() => $`git config core.fsmonitor true`.cwd(dir).quiet())
|
yield* Effect.promise(() => $`git config core.fsmonitor true`.cwd(dir).quiet())
|
||||||
yield* Effect.promise(() => $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow())
|
yield* Effect.promise(() => $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow())
|
||||||
yield* Effect.promise(() => Bun.write(path.join(dir, "tracked.txt"), "next\n"))
|
yield* Effect.promise(() => Bun.write(path.join(dir, "tracked.txt"), "next\n"))
|
||||||
yield* Effect.promise(() => $`git diff`.cwd(dir).quiet())
|
yield* Effect.promise(() => $`git diff`.cwd(dir).quiet())
|
||||||
|
|
||||||
const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow())
|
const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow())
|
||||||
expect(before.exitCode).toBe(0)
|
expect(before.exitCode).toBe(0)
|
||||||
|
|
||||||
const ok = yield* svc.remove({ directory: dir })
|
const ok = yield* svc.remove({ directory: dir })
|
||||||
|
|
||||||
expect(ok).toBe(true)
|
expect(ok).toBe(true)
|
||||||
expect(
|
expect(
|
||||||
yield* Effect.promise(() =>
|
yield* Effect.promise(() =>
|
||||||
fs
|
fs
|
||||||
.stat(dir)
|
.stat(dir)
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false),
|
.catch(() => false),
|
||||||
),
|
),
|
||||||
).toBe(false)
|
).toBe(false)
|
||||||
|
|
||||||
const ref = yield* Effect.promise(() =>
|
const ref = yield* Effect.promise(() =>
|
||||||
$`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
|
$`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
|
||||||
)
|
)
|
||||||
expect(ref.exitCode).not.toBe(0)
|
expect(ref.exitCode).not.toBe(0)
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,9 @@ it.instance(
|
||||||
yield* set("AWS_PROFILE", "default")
|
yield* set("AWS_PROFILE", "default")
|
||||||
const providers = yield* list
|
const providers = yield* list
|
||||||
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
|
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
|
||||||
expect(providers[ProviderV2.ID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
expect(
|
||||||
|
providers[ProviderV2.ID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"],
|
||||||
|
).toBeDefined()
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
config: {
|
config: {
|
||||||
|
|
|
||||||
|
|
@ -245,9 +245,9 @@ it.instance(
|
||||||
expect(provider.models["deepseek-r1"].capabilities.interleaved).toEqual({ field: "reasoning_content" })
|
expect(provider.models["deepseek-r1"].capabilities.interleaved).toEqual({ field: "reasoning_content" })
|
||||||
expect(provider.models["deepseek-details"].capabilities.interleaved).toEqual({ field: "reasoning_details" })
|
expect(provider.models["deepseek-details"].capabilities.interleaved).toEqual({ field: "reasoning_details" })
|
||||||
expect(provider.models["custom-model"].capabilities.interleaved).toBe(false)
|
expect(provider.models["custom-model"].capabilities.interleaved).toBe(false)
|
||||||
expect(providers[ProviderV2.ID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved).toBe(
|
expect(
|
||||||
false,
|
providers[ProviderV2.ID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved,
|
||||||
)
|
).toBe(false)
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
config: {
|
config: {
|
||||||
|
|
@ -305,7 +305,9 @@ it.instance("getModel returns model for valid provider/model", () =>
|
||||||
it.instance("getModel throws ModelNotFoundError for invalid model", () =>
|
it.instance("getModel throws ModelNotFoundError for invalid model", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
const exit = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("nonexistent-model")).pipe(Effect.exit)
|
const exit = yield* Provider.use
|
||||||
|
.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("nonexistent-model"))
|
||||||
|
.pipe(Effect.exit)
|
||||||
expect(exit._tag).toBe("Failure")
|
expect(exit._tag).toBe("Failure")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -977,8 +979,14 @@ it.instance(
|
||||||
it.instance("getModel returns consistent results", () =>
|
it.instance("getModel returns consistent results", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
const model1 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonnet-4-20250514"))
|
const model1 = yield* Provider.use.getModel(
|
||||||
const model2 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonnet-4-20250514"))
|
ProviderV2.ID.anthropic,
|
||||||
|
ProviderV2.ModelID.make("claude-sonnet-4-20250514"),
|
||||||
|
)
|
||||||
|
const model2 = yield* Provider.use.getModel(
|
||||||
|
ProviderV2.ID.anthropic,
|
||||||
|
ProviderV2.ModelID.make("claude-sonnet-4-20250514"),
|
||||||
|
)
|
||||||
expect(model1.providerID).toEqual(model2.providerID)
|
expect(model1.providerID).toEqual(model2.providerID)
|
||||||
expect(model1.id).toEqual(model2.id)
|
expect(model1.id).toEqual(model2.id)
|
||||||
expect(model1).toEqual(model2)
|
expect(model1).toEqual(model2)
|
||||||
|
|
@ -1008,7 +1016,9 @@ it.instance(
|
||||||
it.instance("ModelNotFoundError includes suggestions for typos", () =>
|
it.instance("ModelNotFoundError includes suggestions for typos", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
const error = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonet-4")).pipe(Effect.flip)
|
const error = yield* Provider.use
|
||||||
|
.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonet-4"))
|
||||||
|
.pipe(Effect.flip)
|
||||||
expect(error.suggestions).toBeDefined()
|
expect(error.suggestions).toBeDefined()
|
||||||
expect((error.suggestions ?? []).length).toBeGreaterThan(0)
|
expect((error.suggestions ?? []).length).toBeGreaterThan(0)
|
||||||
}),
|
}),
|
||||||
|
|
@ -1565,7 +1575,10 @@ it.instance("Google Vertex: uses REP endpoint for Claude continental multi-regio
|
||||||
yield* set("GOOGLE_CLOUD_PROJECT", "test-project")
|
yield* set("GOOGLE_CLOUD_PROJECT", "test-project")
|
||||||
yield* set("VERTEX_LOCATION", "eu")
|
yield* set("VERTEX_LOCATION", "eu")
|
||||||
const provider = yield* Provider.Service
|
const provider = yield* Provider.Service
|
||||||
const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ProviderV2.ModelID.make("claude-sonnet-4-6@default"))
|
const model = yield* provider.getModel(
|
||||||
|
ProviderV2.ID.make("google-vertex"),
|
||||||
|
ProviderV2.ModelID.make("claude-sonnet-4-6@default"),
|
||||||
|
)
|
||||||
const language = yield* provider.getLanguage(model)
|
const language = yield* provider.getLanguage(model)
|
||||||
expect(languageBaseURL(language)).toBe(
|
expect(languageBaseURL(language)).toBe(
|
||||||
"https://aiplatform.eu.rep.googleapis.com/v1/projects/test-project/locations/eu/publishers/anthropic/models",
|
"https://aiplatform.eu.rep.googleapis.com/v1/projects/test-project/locations/eu/publishers/anthropic/models",
|
||||||
|
|
@ -1594,7 +1607,10 @@ it.instance("Google Vertex: keeps regional Claude endpoints unchanged", () =>
|
||||||
yield* set("GOOGLE_CLOUD_PROJECT", "test-project")
|
yield* set("GOOGLE_CLOUD_PROJECT", "test-project")
|
||||||
yield* set("VERTEX_LOCATION", "europe-west1")
|
yield* set("VERTEX_LOCATION", "europe-west1")
|
||||||
const provider = yield* Provider.Service
|
const provider = yield* Provider.Service
|
||||||
const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ProviderV2.ModelID.make("claude-sonnet-4-6@default"))
|
const model = yield* provider.getModel(
|
||||||
|
ProviderV2.ID.make("google-vertex"),
|
||||||
|
ProviderV2.ModelID.make("claude-sonnet-4-6@default"),
|
||||||
|
)
|
||||||
const language = yield* provider.getLanguage(model)
|
const language = yield* provider.getLanguage(model)
|
||||||
expect(languageBaseURL(language)).toBe(
|
expect(languageBaseURL(language)).toBe(
|
||||||
"https://europe-west1-aiplatform.googleapis.com/v1/projects/test-project/locations/europe-west1/publishers/anthropic/models",
|
"https://europe-west1-aiplatform.googleapis.com/v1/projects/test-project/locations/europe-west1/publishers/anthropic/models",
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,9 @@ describe("PTY websocket tickets", () => {
|
||||||
const workspaceID = WorkspaceV2.ID.ascending()
|
const workspaceID = WorkspaceV2.ID.ascending()
|
||||||
const issued = yield* tickets.issue({ ptyID, workspaceID })
|
const issued = yield* tickets.issue({ ptyID, workspaceID })
|
||||||
|
|
||||||
expect(yield* tickets.consume({ ptyID, workspaceID: WorkspaceV2.ID.ascending(), ticket: issued.ticket })).toBe(false)
|
expect(yield* tickets.consume({ ptyID, workspaceID: WorkspaceV2.ID.ascending(), ticket: issued.ticket })).toBe(
|
||||||
|
false,
|
||||||
|
)
|
||||||
expect(yield* tickets.consume({ ptyID, workspaceID, ticket: issued.ticket })).toBe(true)
|
expect(yield* tickets.consume({ ptyID, workspaceID, ticket: issued.ticket })).toBe(true)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,11 @@ const it = testEffect(
|
||||||
Layer.mergeAll(Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)), CrossSpawnSpawner.defaultLayer),
|
Layer.mergeAll(Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)), CrossSpawnSpawner.defaultLayer),
|
||||||
)
|
)
|
||||||
const lifecycle = testEffect(
|
const lifecycle = testEffect(
|
||||||
Layer.mergeAll(Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)), CrossSpawnSpawner.defaultLayer, testInstanceStoreLayer),
|
Layer.mergeAll(
|
||||||
|
Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)),
|
||||||
|
CrossSpawnSpawner.defaultLayer,
|
||||||
|
testInstanceStoreLayer,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const askEffect = Effect.fn("QuestionTest.ask")(function* (input: {
|
const askEffect = Effect.fn("QuestionTest.ask")(function* (input: {
|
||||||
|
|
|
||||||
|
|
@ -94,5 +94,4 @@ describe("event HttpApi", () => {
|
||||||
}),
|
}),
|
||||||
{ git: true, config: { formatter: false, lsp: false } },
|
{ git: true, config: { formatter: false, lsp: false } },
|
||||||
)
|
)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,11 @@ const insertRemoteWorkspaceWithoutSync = (input: {
|
||||||
const id = WorkspaceV2.ID.ascending()
|
const id = WorkspaceV2.ID.ascending()
|
||||||
registerAdapter(input.projectID, input.type, remoteAdapter(path.join(input.dir, `.${input.type}`), input.url))
|
registerAdapter(input.projectID, input.type, remoteAdapter(path.join(input.dir, `.${input.type}`), input.url))
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
yield* db.insert(WorkspaceTable).values({ id, type: input.type, project_id: input.projectID }).run().pipe(Effect.orDie)
|
yield* db
|
||||||
|
.insert(WorkspaceTable)
|
||||||
|
.values({ id, type: input.type, project_id: input.projectID })
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
return id
|
return id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -331,7 +335,9 @@ describe("HttpApi workspace routing middleware", () => {
|
||||||
const project = yield* Project.use.fromDirectory(dir)
|
const project = yield* Project.use.fromDirectory(dir)
|
||||||
const workspaceID = WorkspaceV2.ID.ascending()
|
const workspaceID = WorkspaceV2.ID.ascending()
|
||||||
const type = "remote-http-fence-target"
|
const type = "remote-http-fence-target"
|
||||||
const waited = yield* Ref.make<{ workspaceID: WorkspaceV2.ID; state: Record<string, number> } | undefined>(undefined)
|
const waited = yield* Ref.make<{ workspaceID: WorkspaceV2.ID; state: Record<string, number> } | undefined>(
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
|
||||||
const remoteUrl = yield* startRemoteWorkspaceHttpServer(() =>
|
const remoteUrl = yield* startRemoteWorkspaceHttpServer(() =>
|
||||||
HttpServerResponse.json(
|
HttpServerResponse.json(
|
||||||
|
|
|
||||||
|
|
@ -154,8 +154,18 @@ describe("session.list", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
yield* db.update(SessionTable).set({ path: null }).where(eq(SessionTable.id, current.id)).run().pipe(Effect.orDie)
|
yield* db
|
||||||
yield* db.update(SessionTable).set({ path: null }).where(eq(SessionTable.id, sibling.id)).run().pipe(Effect.orDie)
|
.update(SessionTable)
|
||||||
|
.set({ path: null })
|
||||||
|
.where(eq(SessionTable.id, current.id))
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
yield* db
|
||||||
|
.update(SessionTable)
|
||||||
|
.set({ path: null })
|
||||||
|
.where(eq(SessionTable.id, sibling.id))
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
|
||||||
const pathIDs = (yield* SessionNs.Service.use((session) =>
|
const pathIDs = (yield* SessionNs.Service.use((session) =>
|
||||||
session.list({
|
session.list({
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,12 @@ const env = Layer.mergeAll(
|
||||||
|
|
||||||
const it = testEffect(env)
|
const it = testEffect(env)
|
||||||
|
|
||||||
const compactionEnv = Layer.mergeAll(SessionNs.defaultLayer, Database.defaultLayer, EventV2Bridge.defaultLayer, CrossSpawnSpawner.defaultLayer)
|
const compactionEnv = Layer.mergeAll(
|
||||||
|
SessionNs.defaultLayer,
|
||||||
|
Database.defaultLayer,
|
||||||
|
EventV2Bridge.defaultLayer,
|
||||||
|
CrossSpawnSpawner.defaultLayer,
|
||||||
|
)
|
||||||
const itCompaction = testEffect(compactionEnv)
|
const itCompaction = testEffect(compactionEnv)
|
||||||
|
|
||||||
type CompactionProcessOptions = {
|
type CompactionProcessOptions = {
|
||||||
|
|
@ -587,7 +592,6 @@ describe("session.compaction.create", () => {
|
||||||
auto: true,
|
auto: true,
|
||||||
overflow: true,
|
overflow: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -852,7 +856,8 @@ describe("session.compaction.process", () => {
|
||||||
let seen = false
|
let seen = false
|
||||||
const unsub = yield* events.listen((evt) => {
|
const unsub = yield* events.listen((evt) => {
|
||||||
if (evt.type !== SessionCompaction.Event.Compacted.type) return Effect.void
|
if (evt.type !== SessionCompaction.Event.Compacted.type) return Effect.void
|
||||||
if ((evt.data as typeof SessionCompaction.Event.Compacted.data.Type).sessionID !== session.id) return Effect.void
|
if ((evt.data as typeof SessionCompaction.Event.Compacted.data.Type).sessionID !== session.id)
|
||||||
|
return Effect.void
|
||||||
seen = true
|
seen = true
|
||||||
Deferred.doneUnsafe(done, Effect.void)
|
Deferred.doneUnsafe(done, Effect.void)
|
||||||
return Effect.void
|
return Effect.void
|
||||||
|
|
|
||||||
|
|
@ -1617,7 +1617,10 @@ describe("session.llm.stream", () => {
|
||||||
]
|
]
|
||||||
const request = waitRequest("/messages", createEventResponse(chunks))
|
const request = waitRequest("/messages", createEventResponse(chunks))
|
||||||
|
|
||||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.make("anthropic"), ProviderV2.ModelID.make(model.id))
|
const resolved = yield* Provider.use.getModel(
|
||||||
|
ProviderV2.ID.make("anthropic"),
|
||||||
|
ProviderV2.ModelID.make(model.id),
|
||||||
|
)
|
||||||
const sessionID = SessionID.make("session-test-anthropic-tools")
|
const sessionID = SessionID.make("session-test-anthropic-tools")
|
||||||
const agent = {
|
const agent = {
|
||||||
name: "test",
|
name: "test",
|
||||||
|
|
@ -1816,7 +1819,10 @@ describe("session.llm.stream", () => {
|
||||||
]
|
]
|
||||||
const request = waitRequest(pathSuffix, createEventResponse(chunks))
|
const request = waitRequest(pathSuffix, createEventResponse(chunks))
|
||||||
|
|
||||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.make(geminiFixture.providerID), ProviderV2.ModelID.make(model.id))
|
const resolved = yield* Provider.use.getModel(
|
||||||
|
ProviderV2.ID.make(geminiFixture.providerID),
|
||||||
|
ProviderV2.ModelID.make(model.id),
|
||||||
|
)
|
||||||
const sessionID = SessionID.make("session-test-4")
|
const sessionID = SessionID.make("session-test-4")
|
||||||
const agent = {
|
const agent = {
|
||||||
name: "test",
|
name: "test",
|
||||||
|
|
|
||||||
|
|
@ -991,7 +991,9 @@ describe("session.message-v2.toModelMessage", () => {
|
||||||
const assistantID1 = "m-assistant-1"
|
const assistantID1 = "m-assistant-1"
|
||||||
const assistantID2 = "m-assistant-2"
|
const assistantID2 = "m-assistant-2"
|
||||||
|
|
||||||
const aborted = new SessionLegacy.AbortedError({ message: "aborted" }).toObject() as SessionLegacy.Assistant["error"]
|
const aborted = new SessionLegacy.AbortedError({
|
||||||
|
message: "aborted",
|
||||||
|
}).toObject() as SessionLegacy.Assistant["error"]
|
||||||
|
|
||||||
const input: SessionLegacy.WithParts[] = [
|
const input: SessionLegacy.WithParts[] = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -539,7 +539,12 @@ noLLMServer.instance.skip(
|
||||||
Effect.provide(SessionV2.defaultLayer),
|
Effect.provide(SessionV2.defaultLayer),
|
||||||
)
|
)
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
const row = yield* db.select().from(SessionMessageTable).where(eq(SessionMessageTable.session_id, chat.id)).get().pipe(Effect.orDie)
|
const row = yield* db
|
||||||
|
.select()
|
||||||
|
.from(SessionMessageTable)
|
||||||
|
.where(eq(SessionMessageTable.session_id, chat.id))
|
||||||
|
.get()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
expect(messages.find((message) => message.type === "user")).toMatchObject({ type: "user", text: "hello v2" })
|
expect(messages.find((message) => message.type === "user")).toMatchObject({ type: "user", text: "hello v2" })
|
||||||
expect(typeof row?.data.time.created).toBe("number")
|
expect(typeof row?.data.time.created).toBe("number")
|
||||||
expect(messages).toEqual(
|
expect(messages).toEqual(
|
||||||
|
|
|
||||||
|
|
@ -87,31 +87,31 @@ describe("session.retry.delay", () => {
|
||||||
|
|
||||||
it.instance("policy updates retry status and increments attempts", () =>
|
it.instance("policy updates retry status and increments attempts", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const sessionID = SessionID.make("session-retry-test")
|
const sessionID = SessionID.make("session-retry-test")
|
||||||
const error = apiError({ "retry-after-ms": "0" })
|
const error = apiError({ "retry-after-ms": "0" })
|
||||||
const status = yield* SessionStatus.Service
|
const status = yield* SessionStatus.Service
|
||||||
|
|
||||||
const step = yield* Schedule.toStepWithMetadata(
|
const step = yield* Schedule.toStepWithMetadata(
|
||||||
SessionRetry.policy({
|
SessionRetry.policy({
|
||||||
provider: "test",
|
provider: "test",
|
||||||
parse: Schema.decodeUnknownSync(SessionLegacy.APIError.Schema),
|
parse: Schema.decodeUnknownSync(SessionLegacy.APIError.Schema),
|
||||||
set: (info) =>
|
set: (info) =>
|
||||||
status.set(sessionID, {
|
status.set(sessionID, {
|
||||||
type: "retry",
|
type: "retry",
|
||||||
attempt: info.attempt,
|
attempt: info.attempt,
|
||||||
message: info.message,
|
message: info.message,
|
||||||
next: info.next,
|
next: info.next,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* step(error)
|
yield* step(error)
|
||||||
yield* step(error)
|
yield* step(error)
|
||||||
|
|
||||||
expect(yield* status.get(sessionID)).toMatchObject({
|
expect(yield* status.get(sessionID)).toMatchObject({
|
||||||
type: "retry",
|
type: "retry",
|
||||||
attempt: 2,
|
attempt: 2,
|
||||||
message: "boom",
|
message: "boom",
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,10 @@ describe("session.created event", () => {
|
||||||
|
|
||||||
const unsub = yield* events.listen((event) => {
|
const unsub = yield* events.listen((event) => {
|
||||||
if (event.type === SessionNs.Event.Created.type)
|
if (event.type === SessionNs.Event.Created.type)
|
||||||
Deferred.doneUnsafe(received, Effect.succeed((event.data as typeof SessionNs.Event.Created.data.Type).info as SessionNs.Info))
|
Deferred.doneUnsafe(
|
||||||
|
received,
|
||||||
|
Effect.succeed((event.data as typeof SessionNs.Event.Created.data.Type).info as SessionNs.Info),
|
||||||
|
)
|
||||||
return Effect.void
|
return Effect.void
|
||||||
})
|
})
|
||||||
yield* Effect.addFinalizer(() => unsub)
|
yield* Effect.addFinalizer(() => unsub)
|
||||||
|
|
@ -127,7 +130,10 @@ describe("step-finish token propagation via event", () => {
|
||||||
const received = yield* Deferred.make<SessionLegacy.Part>()
|
const received = yield* Deferred.make<SessionLegacy.Part>()
|
||||||
const unsub = yield* events.listen((event) => {
|
const unsub = yield* events.listen((event) => {
|
||||||
if (event.type === MessageV2.Event.PartUpdated.type)
|
if (event.type === MessageV2.Event.PartUpdated.type)
|
||||||
Deferred.doneUnsafe(received, Effect.succeed((event.data as typeof MessageV2.Event.PartUpdated.data.Type).part as SessionLegacy.Part))
|
Deferred.doneUnsafe(
|
||||||
|
received,
|
||||||
|
Effect.succeed((event.data as typeof MessageV2.Event.PartUpdated.data.Type).part as SessionLegacy.Part),
|
||||||
|
)
|
||||||
return Effect.void
|
return Effect.void
|
||||||
})
|
})
|
||||||
yield* Effect.addFinalizer(() => unsub)
|
yield* Effect.addFinalizer(() => unsub)
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,12 @@ function wired(client: HttpClient.HttpClient) {
|
||||||
const share = (id: SessionID) =>
|
const share = (id: SessionID) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
return yield* db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, id)).get().pipe(Effect.orDie)
|
return yield* db
|
||||||
|
.select()
|
||||||
|
.from(SessionShareTable)
|
||||||
|
.where(eq(SessionShareTable.session_id, id))
|
||||||
|
.get()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
})
|
})
|
||||||
|
|
||||||
const seed = (url: string, org?: string) =>
|
const seed = (url: string, org?: string) =>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,13 @@ import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Fiber, Layer } from "effect"
|
import { Effect, Fiber, Layer } from "effect"
|
||||||
import { Snapshot } from "../../src/snapshot"
|
import { Snapshot } from "../../src/snapshot"
|
||||||
import { disposeAllInstances, provideInstance, testInstanceStoreLayer, TestInstance, tmpdirScoped } from "../fixture/fixture"
|
import {
|
||||||
|
disposeAllInstances,
|
||||||
|
provideInstance,
|
||||||
|
testInstanceStoreLayer,
|
||||||
|
TestInstance,
|
||||||
|
tmpdirScoped,
|
||||||
|
} from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(Snapshot.defaultLayer, AppFileSystem.defaultLayer, testInstanceStoreLayer))
|
const it = testEffect(Layer.mergeAll(Snapshot.defaultLayer, AppFileSystem.defaultLayer, testInstanceStoreLayer))
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,10 @@ function migrations() {
|
||||||
.map((entry) => ({
|
.map((entry) => ({
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
timestamp: Number(entry.name.split("_")[0]),
|
timestamp: Number(entry.name.split("_")[0]),
|
||||||
sql: readFileSync(path.join(import.meta.dirname, "../../../core/migration", entry.name, "migration.sql"), "utf-8"),
|
sql: readFileSync(
|
||||||
|
path.join(import.meta.dirname, "../../../core/migration", entry.name, "migration.sql"),
|
||||||
|
"utf-8",
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.timestamp - b.timestamp)
|
.sort((a, b) => a.timestamp - b.timestamp)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,22 +102,22 @@ describe("tool.lsp", () => {
|
||||||
"keeps cursor details for position-based operations",
|
"keeps cursor details for position-based operations",
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
const file = path.join(dir, "test.ts")
|
const file = path.join(dir, "test.ts")
|
||||||
yield* put(file)
|
yield* put(file)
|
||||||
|
|
||||||
const { items, next } = asks()
|
const { items, next } = asks()
|
||||||
const result = yield* run({ operation: "goToDefinition", filePath: file, line: 3, character: 7 }, next)
|
const result = yield* run({ operation: "goToDefinition", filePath: file, line: 3, character: 7 }, next)
|
||||||
const req = items.find((item) => item.permission === "lsp")
|
const req = items.find((item) => item.permission === "lsp")
|
||||||
|
|
||||||
expect(req).toBeDefined()
|
expect(req).toBeDefined()
|
||||||
expect(req!.metadata).toEqual({
|
expect(req!.metadata).toEqual({
|
||||||
operation: "goToDefinition",
|
operation: "goToDefinition",
|
||||||
filePath: file,
|
filePath: file,
|
||||||
line: 3,
|
line: 3,
|
||||||
character: 7,
|
character: 7,
|
||||||
})
|
})
|
||||||
expect(result.title).toBe("goToDefinition test.ts:3:7")
|
expect(result.title).toBe("goToDefinition test.ts:3:7")
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
@ -126,20 +126,20 @@ describe("tool.lsp", () => {
|
||||||
"omits cursor details for documentSymbol",
|
"omits cursor details for documentSymbol",
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
const file = path.join(dir, "test.ts")
|
const file = path.join(dir, "test.ts")
|
||||||
yield* put(file)
|
yield* put(file)
|
||||||
|
|
||||||
const { items, next } = asks()
|
const { items, next } = asks()
|
||||||
const result = yield* run({ operation: "documentSymbol", filePath: file, line: 3, character: 7 }, next)
|
const result = yield* run({ operation: "documentSymbol", filePath: file, line: 3, character: 7 }, next)
|
||||||
const req = items.find((item) => item.permission === "lsp")
|
const req = items.find((item) => item.permission === "lsp")
|
||||||
|
|
||||||
expect(req).toBeDefined()
|
expect(req).toBeDefined()
|
||||||
expect(req!.metadata).toEqual({
|
expect(req!.metadata).toEqual({
|
||||||
operation: "documentSymbol",
|
operation: "documentSymbol",
|
||||||
filePath: file,
|
filePath: file,
|
||||||
})
|
})
|
||||||
expect(result.title).toBe("documentSymbol test.ts")
|
expect(result.title).toBe("documentSymbol test.ts")
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
@ -148,20 +148,20 @@ describe("tool.lsp", () => {
|
||||||
"omits file and cursor details for workspaceSymbol",
|
"omits file and cursor details for workspaceSymbol",
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
workspaceSymbolQueries.length = 0
|
workspaceSymbolQueries.length = 0
|
||||||
const file = path.join(dir, "test.ts")
|
const file = path.join(dir, "test.ts")
|
||||||
yield* put(file)
|
yield* put(file)
|
||||||
|
|
||||||
const { items, next } = asks()
|
const { items, next } = asks()
|
||||||
const result = yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 }, next)
|
const result = yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 }, next)
|
||||||
const req = items.find((item) => item.permission === "lsp")
|
const req = items.find((item) => item.permission === "lsp")
|
||||||
|
|
||||||
expect(req).toBeDefined()
|
expect(req).toBeDefined()
|
||||||
expect(req!.metadata).toEqual({
|
expect(req!.metadata).toEqual({
|
||||||
operation: "workspaceSymbol",
|
operation: "workspaceSymbol",
|
||||||
})
|
})
|
||||||
expect(result.title).toBe("workspaceSymbol")
|
expect(result.title).toBe("workspaceSymbol")
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
@ -170,15 +170,15 @@ describe("tool.lsp", () => {
|
||||||
"passes workspaceSymbol query to LSP",
|
"passes workspaceSymbol query to LSP",
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
workspaceSymbolQueries.length = 0
|
workspaceSymbolQueries.length = 0
|
||||||
const file = path.join(dir, "test.ts")
|
const file = path.join(dir, "test.ts")
|
||||||
yield* put(file)
|
yield* put(file)
|
||||||
|
|
||||||
yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7, query: "TestSymbol" })
|
yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7, query: "TestSymbol" })
|
||||||
yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 })
|
yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 })
|
||||||
|
|
||||||
expect(workspaceSymbolQueries).toEqual(["TestSymbol", ""])
|
expect(workspaceSymbolQueries).toEqual(["TestSymbol", ""])
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,13 @@ import { ReadTool } from "../../src/tool/read"
|
||||||
import { Truncate } from "@/tool/truncate"
|
import { Truncate } from "@/tool/truncate"
|
||||||
import { Tool } from "@/tool/tool"
|
import { Tool } from "@/tool/tool"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { disposeAllInstances, provideInstance, testInstanceStoreLayer, TestInstance, tmpdirScoped } from "../fixture/fixture"
|
import {
|
||||||
|
disposeAllInstances,
|
||||||
|
provideInstance,
|
||||||
|
testInstanceStoreLayer,
|
||||||
|
TestInstance,
|
||||||
|
tmpdirScoped,
|
||||||
|
} from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { Reference } from "@/reference/reference"
|
import { Reference } from "@/reference/reference"
|
||||||
import { RepositoryCache } from "@/reference/repository-cache"
|
import { RepositoryCache } from "@/reference/repository-cache"
|
||||||
|
|
|
||||||
|
|
@ -82,153 +82,153 @@ const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
|
||||||
describe("tool.repo_clone", () => {
|
describe("tool.repo_clone", () => {
|
||||||
it.instance("clones a repo into the managed cache and reuses it on subsequent calls", () =>
|
it.instance("clones a repo into the managed cache and reuses it on subsequent calls", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
const source = yield* tmpdirScoped({ git: true })
|
const source = yield* tmpdirScoped({ git: true })
|
||||||
const remoteRoot = yield* tmpdirScoped()
|
const remoteRoot = yield* tmpdirScoped()
|
||||||
const remoteDir = path.join(remoteRoot, "owner")
|
const remoteDir = path.join(remoteRoot, "owner")
|
||||||
const remoteRepo = path.join(remoteDir, "repo.git")
|
const remoteRepo = path.join(remoteDir, "repo.git")
|
||||||
|
|
||||||
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n"))
|
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n"))
|
||||||
yield* git(source, ["add", "."])
|
yield* git(source, ["add", "."])
|
||||||
yield* git(source, ["commit", "-m", "add readme"])
|
yield* git(source, ["commit", "-m", "add readme"])
|
||||||
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
||||||
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
||||||
|
|
||||||
const tool = yield* init()
|
const tool = yield* init()
|
||||||
const cloned = yield* githubBase(`file://${remoteRoot}/`, tool.execute({ repository: "owner/repo" }, ctx))
|
const cloned = yield* githubBase(`file://${remoteRoot}/`, tool.execute({ repository: "owner/repo" }, ctx))
|
||||||
const cached = yield* githubBase(
|
const cached = yield* githubBase(
|
||||||
`file://${remoteRoot}/`,
|
`file://${remoteRoot}/`,
|
||||||
tool.execute({ repository: "https://github.com/owner/repo.git" }, ctx),
|
tool.execute({ repository: "https://github.com/owner/repo.git" }, ctx),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(cloned.metadata.status).toBe("cloned")
|
expect(cloned.metadata.status).toBe("cloned")
|
||||||
expect(cloned.metadata.localPath).toBe(path.join(Global.Path.repos, "github.com", "owner", "repo"))
|
expect(cloned.metadata.localPath).toBe(path.join(Global.Path.repos, "github.com", "owner", "repo"))
|
||||||
expect(cached.metadata.status).toBe("cached")
|
expect(cached.metadata.status).toBe("cached")
|
||||||
expect(yield* fs.readFileString(path.join(cloned.metadata.localPath, "README.md"))).toBe("v1\n")
|
expect(yield* fs.readFileString(path.join(cloned.metadata.localPath, "README.md"))).toBe("v1\n")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("refresh updates an existing cached clone", () =>
|
it.instance("refresh updates an existing cached clone", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
const source = yield* tmpdirScoped({ git: true })
|
const source = yield* tmpdirScoped({ git: true })
|
||||||
const remoteRoot = yield* tmpdirScoped()
|
const remoteRoot = yield* tmpdirScoped()
|
||||||
const remoteDir = path.join(remoteRoot, "owner")
|
const remoteDir = path.join(remoteRoot, "owner")
|
||||||
const remoteRepo = path.join(remoteDir, "repo.git")
|
const remoteRepo = path.join(remoteDir, "repo.git")
|
||||||
|
|
||||||
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n"))
|
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n"))
|
||||||
yield* git(source, ["add", "."])
|
yield* git(source, ["add", "."])
|
||||||
yield* git(source, ["commit", "-m", "add readme"])
|
yield* git(source, ["commit", "-m", "add readme"])
|
||||||
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
||||||
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
||||||
|
|
||||||
const branch = yield* git(source, ["branch", "--show-current"])
|
const branch = yield* git(source, ["branch", "--show-current"])
|
||||||
yield* git(source, ["remote", "add", "origin", remoteRepo])
|
yield* git(source, ["remote", "add", "origin", remoteRepo])
|
||||||
yield* git(source, ["push", "-u", "origin", `${branch}:${branch}`])
|
yield* git(source, ["push", "-u", "origin", `${branch}:${branch}`])
|
||||||
|
|
||||||
const tool = yield* init()
|
const tool = yield* init()
|
||||||
const first = yield* githubBase(`file://${remoteRoot}/`, tool.execute({ repository: "owner/repo" }, ctx))
|
const first = yield* githubBase(`file://${remoteRoot}/`, tool.execute({ repository: "owner/repo" }, ctx))
|
||||||
|
|
||||||
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v2\n"))
|
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v2\n"))
|
||||||
yield* git(source, ["add", "."])
|
yield* git(source, ["add", "."])
|
||||||
yield* git(source, ["commit", "-m", "update readme"])
|
yield* git(source, ["commit", "-m", "update readme"])
|
||||||
yield* git(source, ["push", "origin", `${branch}:${branch}`])
|
yield* git(source, ["push", "origin", `${branch}:${branch}`])
|
||||||
|
|
||||||
const refreshed = yield* githubBase(
|
const refreshed = yield* githubBase(
|
||||||
`file://${remoteRoot}/`,
|
`file://${remoteRoot}/`,
|
||||||
tool.execute({ repository: "owner/repo", refresh: true }, ctx),
|
tool.execute({ repository: "owner/repo", refresh: true }, ctx),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(first.metadata.status).toBe("cloned")
|
expect(first.metadata.status).toBe("cloned")
|
||||||
expect(refreshed.metadata.status).toBe("refreshed")
|
expect(refreshed.metadata.status).toBe("refreshed")
|
||||||
expect(yield* fs.readFileString(path.join(first.metadata.localPath, "README.md"))).toBe("v2\n")
|
expect(yield* fs.readFileString(path.join(first.metadata.localPath, "README.md"))).toBe("v2\n")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("clones a configured branch", () =>
|
it.instance("clones a configured branch", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
const source = yield* tmpdirScoped({ git: true })
|
const source = yield* tmpdirScoped({ git: true })
|
||||||
const remoteRoot = yield* tmpdirScoped()
|
const remoteRoot = yield* tmpdirScoped()
|
||||||
const remoteDir = path.join(remoteRoot, "owner")
|
const remoteDir = path.join(remoteRoot, "owner")
|
||||||
const remoteRepo = path.join(remoteDir, "repo.git")
|
const remoteRepo = path.join(remoteDir, "repo.git")
|
||||||
|
|
||||||
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "main\n"))
|
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "main\n"))
|
||||||
yield* git(source, ["add", "."])
|
yield* git(source, ["add", "."])
|
||||||
yield* git(source, ["commit", "-m", "add readme"])
|
yield* git(source, ["commit", "-m", "add readme"])
|
||||||
yield* git(source, ["checkout", "-b", "docs"])
|
yield* git(source, ["checkout", "-b", "docs"])
|
||||||
yield* Effect.promise(() => Bun.write(path.join(source, "DOCS.md"), "docs\n"))
|
yield* Effect.promise(() => Bun.write(path.join(source, "DOCS.md"), "docs\n"))
|
||||||
yield* git(source, ["add", "."])
|
yield* git(source, ["add", "."])
|
||||||
yield* git(source, ["commit", "-m", "add docs"])
|
yield* git(source, ["commit", "-m", "add docs"])
|
||||||
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
||||||
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
||||||
|
|
||||||
const tool = yield* init()
|
const tool = yield* init()
|
||||||
const result = yield* githubBase(
|
const result = yield* githubBase(
|
||||||
`file://${remoteRoot}/`,
|
`file://${remoteRoot}/`,
|
||||||
tool.execute({ repository: "owner/repo", branch: "docs" }, ctx),
|
tool.execute({ repository: "owner/repo", branch: "docs" }, ctx),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(result.metadata.status).toBe("cloned")
|
expect(result.metadata.status).toBe("cloned")
|
||||||
expect(result.metadata.branch).toBe("docs")
|
expect(result.metadata.branch).toBe("docs")
|
||||||
expect(yield* fs.readFileString(path.join(result.metadata.localPath, "DOCS.md"))).toBe("docs\n")
|
expect(yield* fs.readFileString(path.join(result.metadata.localPath, "DOCS.md"))).toBe("docs\n")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("rejects invalid repository inputs", () =>
|
it.instance("rejects invalid repository inputs", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
const tool = yield* init()
|
const tool = yield* init()
|
||||||
const inputs = [
|
const inputs = [
|
||||||
{ repository: "not-a-repo", message: "git URL" },
|
{ repository: "not-a-repo", message: "git URL" },
|
||||||
{ repository: "git@github.com:../../../etc/passwd", message: "git URL" },
|
{ repository: "git@github.com:../../../etc/passwd", message: "git URL" },
|
||||||
{ repository: "-u:foo/bar", message: "git URL" },
|
{ repository: "-u:foo/bar", message: "git URL" },
|
||||||
{ repository: pathToFileURL(path.join(dir, "local.git")).href, message: "Local file" },
|
{ repository: pathToFileURL(path.join(dir, "local.git")).href, message: "Local file" },
|
||||||
]
|
]
|
||||||
|
|
||||||
yield* Effect.forEach(
|
yield* Effect.forEach(
|
||||||
inputs,
|
inputs,
|
||||||
(input) =>
|
(input) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const result = yield* tool.execute({ repository: input.repository }, ctx).pipe(Effect.exit)
|
const result = yield* tool.execute({ repository: input.repository }, ctx).pipe(Effect.exit)
|
||||||
|
|
||||||
expect(Exit.isFailure(result)).toBe(true)
|
expect(Exit.isFailure(result)).toBe(true)
|
||||||
if (Exit.isFailure(result)) {
|
if (Exit.isFailure(result)) {
|
||||||
const error = Cause.squash(result.cause)
|
const error = Cause.squash(result.cause)
|
||||||
expect(error instanceof Error ? error.message : String(error)).toContain(input.message)
|
expect(error instanceof Error ? error.message : String(error)).toContain(input.message)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{ discard: true },
|
{ discard: true },
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("rejects local file repository URLs", () =>
|
it.instance("rejects local file repository URLs", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const source = yield* tmpdirScoped({ git: true })
|
const source = yield* tmpdirScoped({ git: true })
|
||||||
const tool = yield* init()
|
const tool = yield* init()
|
||||||
const result = yield* tool.execute({ repository: pathToFileURL(source).href }, ctx).pipe(Effect.exit)
|
const result = yield* tool.execute({ repository: pathToFileURL(source).href }, ctx).pipe(Effect.exit)
|
||||||
|
|
||||||
expect(Exit.isFailure(result)).toBe(true)
|
expect(Exit.isFailure(result)).toBe(true)
|
||||||
if (Exit.isFailure(result)) {
|
if (Exit.isFailure(result)) {
|
||||||
const error = Cause.squash(result.cause)
|
const error = Cause.squash(result.cause)
|
||||||
expect(error instanceof Error ? error.message : String(error)).toContain("Local file")
|
expect(error instanceof Error ? error.message : String(error)).toContain("Local file")
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("rejects invalid branch inputs", () =>
|
it.instance("rejects invalid branch inputs", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const tool = yield* init()
|
const tool = yield* init()
|
||||||
const result = yield* tool.execute({ repository: "owner/repo", branch: "bad..branch" }, ctx).pipe(Effect.exit)
|
const result = yield* tool.execute({ repository: "owner/repo", branch: "bad..branch" }, ctx).pipe(Effect.exit)
|
||||||
|
|
||||||
expect(Exit.isFailure(result)).toBe(true)
|
expect(Exit.isFailure(result)).toBe(true)
|
||||||
if (Exit.isFailure(result)) {
|
if (Exit.isFailure(result)) {
|
||||||
const error = Cause.squash(result.cause)
|
const error = Cause.squash(result.cause)
|
||||||
expect(error instanceof Error ? error.message : String(error)).toContain(
|
expect(error instanceof Error ? error.message : String(error)).toContain(
|
||||||
"Branch must contain only alphanumeric characters",
|
"Branch must contain only alphanumeric characters",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -45,112 +45,112 @@ const init = Effect.fn("RepoOverviewToolTest.init")(function* () {
|
||||||
describe("tool.repo_overview", () => {
|
describe("tool.repo_overview", () => {
|
||||||
it.instance("summarizes a local repository path", () =>
|
it.instance("summarizes a local repository path", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const repo = yield* tmpdirScoped({ git: true })
|
const repo = yield* tmpdirScoped({ git: true })
|
||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
yield* fs.writeWithDirs(
|
yield* fs.writeWithDirs(
|
||||||
path.join(repo, "package.json"),
|
path.join(repo, "package.json"),
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
name: "example-repo",
|
name: "example-repo",
|
||||||
main: "dist/index.js",
|
main: "dist/index.js",
|
||||||
module: "dist/index.mjs",
|
module: "dist/index.mjs",
|
||||||
types: "dist/index.d.ts",
|
types: "dist/index.d.ts",
|
||||||
exports: {
|
exports: {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
"./server": "./dist/server.js",
|
"./server": "./dist/server.js",
|
||||||
},
|
|
||||||
bin: {
|
|
||||||
example: "./bin/example.js",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
null,
|
bin: {
|
||||||
2,
|
example: "./bin/example.js",
|
||||||
),
|
},
|
||||||
)
|
},
|
||||||
yield* fs.writeWithDirs(path.join(repo, "bun.lock"), "")
|
null,
|
||||||
yield* fs.writeWithDirs(path.join(repo, "README.md"), "# Example\n")
|
2,
|
||||||
yield* fs.writeWithDirs(path.join(repo, "src", "index.ts"), "export const value = 1\n")
|
),
|
||||||
|
)
|
||||||
|
yield* fs.writeWithDirs(path.join(repo, "bun.lock"), "")
|
||||||
|
yield* fs.writeWithDirs(path.join(repo, "README.md"), "# Example\n")
|
||||||
|
yield* fs.writeWithDirs(path.join(repo, "src", "index.ts"), "export const value = 1\n")
|
||||||
|
|
||||||
const tool = yield* init()
|
const tool = yield* init()
|
||||||
const result = yield* tool.execute({ path: repo }, ctx)
|
const result = yield* tool.execute({ path: repo }, ctx)
|
||||||
|
|
||||||
expect(result.metadata.path).toBe(repo)
|
expect(result.metadata.path).toBe(repo)
|
||||||
expect(result.metadata.ecosystems).toContain("Node.js")
|
expect(result.metadata.ecosystems).toContain("Node.js")
|
||||||
expect(result.metadata.package_manager).toBe("bun")
|
expect(result.metadata.package_manager).toBe("bun")
|
||||||
expect(result.metadata.dependency_files).toEqual(expect.arrayContaining(["package.json", "bun.lock"]))
|
expect(result.metadata.dependency_files).toEqual(expect.arrayContaining(["package.json", "bun.lock"]))
|
||||||
expect(result.metadata.entrypoints).toEqual(
|
expect(result.metadata.entrypoints).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
"main: dist/index.js",
|
"main: dist/index.js",
|
||||||
"module: dist/index.mjs",
|
"module: dist/index.mjs",
|
||||||
"types: dist/index.d.ts",
|
"types: dist/index.d.ts",
|
||||||
"exports: .",
|
"exports: .",
|
||||||
"exports: ./server",
|
"exports: ./server",
|
||||||
"bin: example",
|
"bin: example",
|
||||||
"file: src/index.ts",
|
"file: src/index.ts",
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
expect(result.output).toContain("Top-level structure:")
|
expect(result.output).toContain("Top-level structure:")
|
||||||
expect(result.output).toContain("src/")
|
expect(result.output).toContain("src/")
|
||||||
expect(result.output).toContain("README.md")
|
expect(result.output).toContain("README.md")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("resolves relative paths from the instance directory", () =>
|
it.instance("resolves relative paths from the instance directory", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
yield* fs.writeWithDirs(path.join(dir, "nested", "README.md"), "# Nested\n")
|
yield* fs.writeWithDirs(path.join(dir, "nested", "README.md"), "# Nested\n")
|
||||||
|
|
||||||
const tool = yield* init()
|
const tool = yield* init()
|
||||||
const result = yield* tool.execute({ path: "nested" }, ctx)
|
const result = yield* tool.execute({ path: "nested" }, ctx)
|
||||||
|
|
||||||
expect(result.metadata.path).toBe(path.join(dir, "nested"))
|
expect(result.metadata.path).toBe(path.join(dir, "nested"))
|
||||||
expect(result.output).toContain("README.md")
|
expect(result.output).toContain("README.md")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("resolves a cached repository from repository shorthand", () =>
|
it.instance("resolves a cached repository from repository shorthand", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
const cached = path.join(Global.Path.repos, "github.com", "owner", "repo")
|
const cached = path.join(Global.Path.repos, "github.com", "owner", "repo")
|
||||||
yield* fs.writeWithDirs(path.join(cached, "package.json"), JSON.stringify({ name: "cached-repo" }, null, 2))
|
yield* fs.writeWithDirs(path.join(cached, "package.json"), JSON.stringify({ name: "cached-repo" }, null, 2))
|
||||||
yield* fs.writeWithDirs(path.join(cached, "README.md"), "cached\n")
|
yield* fs.writeWithDirs(path.join(cached, "README.md"), "cached\n")
|
||||||
|
|
||||||
const tool = yield* init()
|
const tool = yield* init()
|
||||||
const result = yield* tool.execute({ repository: "owner/repo" }, ctx)
|
const result = yield* tool.execute({ repository: "owner/repo" }, ctx)
|
||||||
|
|
||||||
expect(result.metadata.path).toBe(cached)
|
expect(result.metadata.path).toBe(cached)
|
||||||
expect(result.metadata.repository).toBe("owner/repo")
|
expect(result.metadata.repository).toBe("owner/repo")
|
||||||
expect(result.output).toContain("Repository: owner/repo")
|
expect(result.output).toContain("Repository: owner/repo")
|
||||||
expect(result.output).toContain(`Path: ${cached}`)
|
expect(result.output).toContain(`Path: ${cached}`)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("fails clearly when a repository is not cloned", () =>
|
it.instance("fails clearly when a repository is not cloned", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const tool = yield* init()
|
const tool = yield* init()
|
||||||
const result = yield* tool.execute({ repository: "missing/repo" }, ctx).pipe(Effect.exit)
|
const result = yield* tool.execute({ repository: "missing/repo" }, ctx).pipe(Effect.exit)
|
||||||
|
|
||||||
expect(Exit.isFailure(result)).toBe(true)
|
expect(Exit.isFailure(result)).toBe(true)
|
||||||
if (Exit.isFailure(result)) {
|
if (Exit.isFailure(result)) {
|
||||||
const error = Cause.squash(result.cause)
|
const error = Cause.squash(result.cause)
|
||||||
expect(error instanceof Error ? error.message : String(error)).toContain("Use repo_clone first")
|
expect(error instanceof Error ? error.message : String(error)).toContain("Use repo_clone first")
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("resolves cached repositories from host/path references", () =>
|
it.instance("resolves cached repositories from host/path references", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
const cached = path.join(Global.Path.repos, "gitlab.com", "group", "repo")
|
const cached = path.join(Global.Path.repos, "gitlab.com", "group", "repo")
|
||||||
yield* fs.writeWithDirs(path.join(cached, "README.md"), "cached\n")
|
yield* fs.writeWithDirs(path.join(cached, "README.md"), "cached\n")
|
||||||
|
|
||||||
const tool = yield* init()
|
const tool = yield* init()
|
||||||
const result = yield* tool.execute({ repository: "gitlab.com/group/repo" }, ctx)
|
const result = yield* tool.execute({ repository: "gitlab.com/group/repo" }, ctx)
|
||||||
|
|
||||||
expect(result.metadata.path).toBe(cached)
|
expect(result.metadata.path).toBe(cached)
|
||||||
expect(result.metadata.repository).toBe("gitlab.com/group/repo")
|
expect(result.metadata.repository).toBe("gitlab.com/group/repo")
|
||||||
expect(result.output).toContain("Repository: gitlab.com/group/repo")
|
expect(result.output).toContain("Repository: gitlab.com/group/repo")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,12 @@ const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
|
||||||
describe("tool.skill", () => {
|
describe("tool.skill", () => {
|
||||||
it.instance("execute returns skill content block with files", () =>
|
it.instance("execute returns skill content block with files", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
const skill = path.join(dir, ".opencode", "skill", "tool-skill")
|
const skill = path.join(dir, ".opencode", "skill", "tool-skill")
|
||||||
yield* Effect.promise(() =>
|
yield* Effect.promise(() =>
|
||||||
Bun.write(
|
Bun.write(
|
||||||
path.join(skill, "SKILL.md"),
|
path.join(skill, "SKILL.md"),
|
||||||
`---
|
`---
|
||||||
name: tool-skill
|
name: tool-skill
|
||||||
description: Skill for tool tests.
|
description: Skill for tool tests.
|
||||||
---
|
---
|
||||||
|
|
@ -46,86 +46,86 @@ description: Skill for tool tests.
|
||||||
|
|
||||||
Use this skill.
|
Use this skill.
|
||||||
`,
|
`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
yield* Effect.promise(() => Bun.write(path.join(skill, "scripts", "demo.txt"), "demo"))
|
yield* Effect.promise(() => Bun.write(path.join(skill, "scripts", "demo.txt"), "demo"))
|
||||||
|
|
||||||
const home = process.env.OPENCODE_TEST_HOME
|
const home = process.env.OPENCODE_TEST_HOME
|
||||||
process.env.OPENCODE_TEST_HOME = dir
|
process.env.OPENCODE_TEST_HOME = dir
|
||||||
yield* Effect.addFinalizer(() =>
|
yield* Effect.addFinalizer(() =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
process.env.OPENCODE_TEST_HOME = home
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
|
||||||
|
const tool = (yield* registry.tools({
|
||||||
|
providerID: "opencode" as any,
|
||||||
|
modelID: "gpt-5" as any,
|
||||||
|
agent,
|
||||||
|
})).find((tool) => tool.id === SkillTool.id)
|
||||||
|
if (!tool) throw new Error("Skill tool not found")
|
||||||
|
|
||||||
|
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||||
|
const ctx: Tool.Context = {
|
||||||
|
...baseCtx,
|
||||||
|
ask: (req) =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
process.env.OPENCODE_TEST_HOME = home
|
requests.push(req)
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
|
|
||||||
const registry = yield* ToolRegistry.Service
|
const result = yield* tool.execute({ name: "tool-skill" }, ctx)
|
||||||
const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
|
const file = path.resolve(skill, "scripts", "demo.txt")
|
||||||
const tool = (yield* registry.tools({
|
|
||||||
providerID: "opencode" as any,
|
|
||||||
modelID: "gpt-5" as any,
|
|
||||||
agent,
|
|
||||||
})).find((tool) => tool.id === SkillTool.id)
|
|
||||||
if (!tool) throw new Error("Skill tool not found")
|
|
||||||
|
|
||||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
expect(requests.length).toBe(1)
|
||||||
const ctx: Tool.Context = {
|
expect(requests[0].permission).toBe("skill")
|
||||||
...baseCtx,
|
expect(requests[0].patterns).toContain("tool-skill")
|
||||||
ask: (req) =>
|
expect(requests[0].always).toContain("tool-skill")
|
||||||
Effect.sync(() => {
|
expect(result.metadata.dir).toBe(skill)
|
||||||
requests.push(req)
|
expect(result.output).toContain(`<skill_content name="tool-skill">`)
|
||||||
}),
|
expect(result.output).toContain(`Base directory for this skill: ${pathToFileURL(skill).href}`)
|
||||||
}
|
expect(result.output).toContain(`<file>${file}</file>`)
|
||||||
|
|
||||||
const result = yield* tool.execute({ name: "tool-skill" }, ctx)
|
|
||||||
const file = path.resolve(skill, "scripts", "demo.txt")
|
|
||||||
|
|
||||||
expect(requests.length).toBe(1)
|
|
||||||
expect(requests[0].permission).toBe("skill")
|
|
||||||
expect(requests[0].patterns).toContain("tool-skill")
|
|
||||||
expect(requests[0].always).toContain("tool-skill")
|
|
||||||
expect(result.metadata.dir).toBe(skill)
|
|
||||||
expect(result.output).toContain(`<skill_content name="tool-skill">`)
|
|
||||||
expect(result.output).toContain(`Base directory for this skill: ${pathToFileURL(skill).href}`)
|
|
||||||
expect(result.output).toContain(`<file>${file}</file>`)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("execute preserves not found message", () =>
|
it.instance("execute preserves not found message", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = (yield* TestInstance).directory
|
const dir = (yield* TestInstance).directory
|
||||||
const home = process.env.OPENCODE_TEST_HOME
|
const home = process.env.OPENCODE_TEST_HOME
|
||||||
process.env.OPENCODE_TEST_HOME = dir
|
process.env.OPENCODE_TEST_HOME = dir
|
||||||
yield* Effect.addFinalizer(() =>
|
yield* Effect.addFinalizer(() =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
process.env.OPENCODE_TEST_HOME = home
|
process.env.OPENCODE_TEST_HOME = home
|
||||||
}),
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
|
||||||
|
const tool = (yield* registry.tools({
|
||||||
|
providerID: "opencode" as any,
|
||||||
|
modelID: "gpt-5" as any,
|
||||||
|
agent,
|
||||||
|
})).find((tool) => tool.id === SkillTool.id)
|
||||||
|
if (!tool) throw new Error("Skill tool not found")
|
||||||
|
|
||||||
|
const exit = yield* tool
|
||||||
|
.execute(
|
||||||
|
{ name: "missing-skill" },
|
||||||
|
{
|
||||||
|
...baseCtx,
|
||||||
|
ask: () => Effect.void,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
.pipe(Effect.exit)
|
||||||
|
|
||||||
const registry = yield* ToolRegistry.Service
|
expect(Exit.isFailure(exit)).toBe(true)
|
||||||
const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
|
if (Exit.isFailure(exit)) {
|
||||||
const tool = (yield* registry.tools({
|
const error = Cause.squash(exit.cause)
|
||||||
providerID: "opencode" as any,
|
expect(error).toBeInstanceOf(Error)
|
||||||
modelID: "gpt-5" as any,
|
if (error instanceof Error) expect(error.message).toContain('Skill "missing-skill" not found.')
|
||||||
agent,
|
}
|
||||||
})).find((tool) => tool.id === SkillTool.id)
|
|
||||||
if (!tool) throw new Error("Skill tool not found")
|
|
||||||
|
|
||||||
const exit = yield* tool
|
|
||||||
.execute(
|
|
||||||
{ name: "missing-skill" },
|
|
||||||
{
|
|
||||||
...baseCtx,
|
|
||||||
ask: () => Effect.void,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.pipe(Effect.exit)
|
|
||||||
|
|
||||||
expect(Exit.isFailure(exit)).toBe(true)
|
|
||||||
if (Exit.isFailure(exit)) {
|
|
||||||
const error = Cause.squash(exit.cause)
|
|
||||||
expect(error).toBeInstanceOf(Error)
|
|
||||||
if (error instanceof Error) expect(error.message).toContain('Skill "missing-skill" not found.')
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -12,39 +12,43 @@ test.skip("step snapshots carry over to assistant messages", () => {
|
||||||
const state: SessionMessageUpdater.MemoryState = { messages: [] }
|
const state: SessionMessageUpdater.MemoryState = { messages: [] }
|
||||||
const sessionID = SessionID.make("session")
|
const sessionID = SessionID.make("session")
|
||||||
|
|
||||||
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
Effect.runSync(
|
||||||
id: EventV2.ID.create(),
|
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||||
type: "session.next.step.started",
|
id: EventV2.ID.create(),
|
||||||
data: {
|
type: "session.next.step.started",
|
||||||
sessionID,
|
data: {
|
||||||
timestamp: DateTime.makeUnsafe(1),
|
sessionID,
|
||||||
agent: "build",
|
timestamp: DateTime.makeUnsafe(1),
|
||||||
model: {
|
agent: "build",
|
||||||
id: ModelV2.ID.make("model"),
|
model: {
|
||||||
providerID: ProviderV2.ID.make("provider"),
|
id: ModelV2.ID.make("model"),
|
||||||
variant: ModelV2.VariantID.make("default"),
|
providerID: ProviderV2.ID.make("provider"),
|
||||||
|
variant: ModelV2.VariantID.make("default"),
|
||||||
|
},
|
||||||
|
snapshot: "before",
|
||||||
},
|
},
|
||||||
snapshot: "before",
|
} satisfies SessionEvent.Event),
|
||||||
},
|
)
|
||||||
} satisfies SessionEvent.Event))
|
|
||||||
|
|
||||||
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
Effect.runSync(
|
||||||
id: EventV2.ID.create(),
|
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||||
type: "session.next.step.ended",
|
id: EventV2.ID.create(),
|
||||||
data: {
|
type: "session.next.step.ended",
|
||||||
sessionID,
|
data: {
|
||||||
timestamp: DateTime.makeUnsafe(2),
|
sessionID,
|
||||||
finish: "stop",
|
timestamp: DateTime.makeUnsafe(2),
|
||||||
cost: 0,
|
finish: "stop",
|
||||||
tokens: {
|
cost: 0,
|
||||||
input: 1,
|
tokens: {
|
||||||
output: 2,
|
input: 1,
|
||||||
reasoning: 0,
|
output: 2,
|
||||||
cache: { read: 0, write: 0 },
|
reasoning: 0,
|
||||||
|
cache: { read: 0, write: 0 },
|
||||||
|
},
|
||||||
|
snapshot: "after",
|
||||||
},
|
},
|
||||||
snapshot: "after",
|
} satisfies SessionEvent.Event),
|
||||||
},
|
)
|
||||||
} satisfies SessionEvent.Event))
|
|
||||||
|
|
||||||
expect(state.messages[0]?.type).toBe("assistant")
|
expect(state.messages[0]?.type).toBe("assistant")
|
||||||
if (state.messages[0]?.type !== "assistant") return
|
if (state.messages[0]?.type !== "assistant") return
|
||||||
|
|
@ -56,39 +60,45 @@ test.skip("text ended populates assistant text content", () => {
|
||||||
const state: SessionMessageUpdater.MemoryState = { messages: [] }
|
const state: SessionMessageUpdater.MemoryState = { messages: [] }
|
||||||
const sessionID = SessionID.make("session")
|
const sessionID = SessionID.make("session")
|
||||||
|
|
||||||
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
Effect.runSync(
|
||||||
id: EventV2.ID.create(),
|
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||||
type: "session.next.step.started",
|
id: EventV2.ID.create(),
|
||||||
data: {
|
type: "session.next.step.started",
|
||||||
sessionID,
|
data: {
|
||||||
timestamp: DateTime.makeUnsafe(1),
|
sessionID,
|
||||||
agent: "build",
|
timestamp: DateTime.makeUnsafe(1),
|
||||||
model: {
|
agent: "build",
|
||||||
id: ModelV2.ID.make("model"),
|
model: {
|
||||||
providerID: ProviderV2.ID.make("provider"),
|
id: ModelV2.ID.make("model"),
|
||||||
variant: ModelV2.VariantID.make("default"),
|
providerID: ProviderV2.ID.make("provider"),
|
||||||
|
variant: ModelV2.VariantID.make("default"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
} satisfies SessionEvent.Event),
|
||||||
} satisfies SessionEvent.Event))
|
)
|
||||||
|
|
||||||
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
Effect.runSync(
|
||||||
id: EventV2.ID.create(),
|
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||||
type: "session.next.text.started",
|
id: EventV2.ID.create(),
|
||||||
data: {
|
type: "session.next.text.started",
|
||||||
sessionID,
|
data: {
|
||||||
timestamp: DateTime.makeUnsafe(2),
|
sessionID,
|
||||||
},
|
timestamp: DateTime.makeUnsafe(2),
|
||||||
} satisfies SessionEvent.Event))
|
},
|
||||||
|
} satisfies SessionEvent.Event),
|
||||||
|
)
|
||||||
|
|
||||||
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
Effect.runSync(
|
||||||
id: EventV2.ID.create(),
|
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||||
type: "session.next.text.ended",
|
id: EventV2.ID.create(),
|
||||||
data: {
|
type: "session.next.text.ended",
|
||||||
sessionID,
|
data: {
|
||||||
timestamp: DateTime.makeUnsafe(3),
|
sessionID,
|
||||||
text: "hello assistant",
|
timestamp: DateTime.makeUnsafe(3),
|
||||||
},
|
text: "hello assistant",
|
||||||
} satisfies SessionEvent.Event))
|
},
|
||||||
|
} satisfies SessionEvent.Event),
|
||||||
|
)
|
||||||
|
|
||||||
expect(state.messages[0]?.type).toBe("assistant")
|
expect(state.messages[0]?.type).toBe("assistant")
|
||||||
if (state.messages[0]?.type !== "assistant") return
|
if (state.messages[0]?.type !== "assistant") return
|
||||||
|
|
@ -100,57 +110,65 @@ test.skip("tool completion stores completed timestamp", () => {
|
||||||
const sessionID = SessionID.make("session")
|
const sessionID = SessionID.make("session")
|
||||||
const callID = "call"
|
const callID = "call"
|
||||||
|
|
||||||
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
Effect.runSync(
|
||||||
id: EventV2.ID.create(),
|
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||||
type: "session.next.step.started",
|
id: EventV2.ID.create(),
|
||||||
data: {
|
type: "session.next.step.started",
|
||||||
sessionID,
|
data: {
|
||||||
timestamp: DateTime.makeUnsafe(1),
|
sessionID,
|
||||||
agent: "build",
|
timestamp: DateTime.makeUnsafe(1),
|
||||||
model: {
|
agent: "build",
|
||||||
id: ModelV2.ID.make("model"),
|
model: {
|
||||||
providerID: ProviderV2.ID.make("provider"),
|
id: ModelV2.ID.make("model"),
|
||||||
variant: ModelV2.VariantID.make("default"),
|
providerID: ProviderV2.ID.make("provider"),
|
||||||
|
variant: ModelV2.VariantID.make("default"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
} satisfies SessionEvent.Event),
|
||||||
} satisfies SessionEvent.Event))
|
)
|
||||||
|
|
||||||
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
Effect.runSync(
|
||||||
id: EventV2.ID.create(),
|
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||||
type: "session.next.tool.input.started",
|
id: EventV2.ID.create(),
|
||||||
data: {
|
type: "session.next.tool.input.started",
|
||||||
sessionID,
|
data: {
|
||||||
timestamp: DateTime.makeUnsafe(2),
|
sessionID,
|
||||||
callID,
|
timestamp: DateTime.makeUnsafe(2),
|
||||||
name: "bash",
|
callID,
|
||||||
},
|
name: "bash",
|
||||||
} satisfies SessionEvent.Event))
|
},
|
||||||
|
} satisfies SessionEvent.Event),
|
||||||
|
)
|
||||||
|
|
||||||
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
Effect.runSync(
|
||||||
id: EventV2.ID.create(),
|
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||||
type: "session.next.tool.called",
|
id: EventV2.ID.create(),
|
||||||
data: {
|
type: "session.next.tool.called",
|
||||||
sessionID,
|
data: {
|
||||||
timestamp: DateTime.makeUnsafe(3),
|
sessionID,
|
||||||
callID,
|
timestamp: DateTime.makeUnsafe(3),
|
||||||
tool: "bash",
|
callID,
|
||||||
input: { command: "pwd" },
|
tool: "bash",
|
||||||
provider: { executed: true, metadata: { source: "provider" } },
|
input: { command: "pwd" },
|
||||||
},
|
provider: { executed: true, metadata: { source: "provider" } },
|
||||||
} satisfies SessionEvent.Event))
|
},
|
||||||
|
} satisfies SessionEvent.Event),
|
||||||
|
)
|
||||||
|
|
||||||
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
Effect.runSync(
|
||||||
id: EventV2.ID.create(),
|
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||||
type: "session.next.tool.success",
|
id: EventV2.ID.create(),
|
||||||
data: {
|
type: "session.next.tool.success",
|
||||||
sessionID,
|
data: {
|
||||||
timestamp: DateTime.makeUnsafe(4),
|
sessionID,
|
||||||
callID,
|
timestamp: DateTime.makeUnsafe(4),
|
||||||
structured: {},
|
callID,
|
||||||
content: [{ type: "text", text: "/tmp" }],
|
structured: {},
|
||||||
provider: { executed: true, metadata: { status: "done" } },
|
content: [{ type: "text", text: "/tmp" }],
|
||||||
},
|
provider: { executed: true, metadata: { status: "done" } },
|
||||||
} satisfies SessionEvent.Event))
|
},
|
||||||
|
} satisfies SessionEvent.Event),
|
||||||
|
)
|
||||||
|
|
||||||
expect(state.messages[0]?.type).toBe("assistant")
|
expect(state.messages[0]?.type).toBe("assistant")
|
||||||
if (state.messages[0]?.type !== "assistant") return
|
if (state.messages[0]?.type !== "assistant") return
|
||||||
|
|
@ -165,46 +183,54 @@ test.skip("compaction events reduce to compaction message", () => {
|
||||||
const sessionID = SessionID.make("session")
|
const sessionID = SessionID.make("session")
|
||||||
const id = EventV2.ID.create()
|
const id = EventV2.ID.create()
|
||||||
|
|
||||||
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
Effect.runSync(
|
||||||
id,
|
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||||
type: "session.next.compaction.started",
|
id,
|
||||||
data: {
|
type: "session.next.compaction.started",
|
||||||
sessionID,
|
data: {
|
||||||
timestamp: DateTime.makeUnsafe(1),
|
sessionID,
|
||||||
reason: "auto",
|
timestamp: DateTime.makeUnsafe(1),
|
||||||
},
|
reason: "auto",
|
||||||
} satisfies SessionEvent.Event))
|
},
|
||||||
|
} satisfies SessionEvent.Event),
|
||||||
|
)
|
||||||
|
|
||||||
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
Effect.runSync(
|
||||||
id: EventV2.ID.create(),
|
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||||
type: "session.next.compaction.delta",
|
id: EventV2.ID.create(),
|
||||||
data: {
|
type: "session.next.compaction.delta",
|
||||||
sessionID,
|
data: {
|
||||||
timestamp: DateTime.makeUnsafe(2),
|
sessionID,
|
||||||
text: "hello ",
|
timestamp: DateTime.makeUnsafe(2),
|
||||||
},
|
text: "hello ",
|
||||||
} satisfies SessionEvent.Event))
|
},
|
||||||
|
} satisfies SessionEvent.Event),
|
||||||
|
)
|
||||||
|
|
||||||
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
Effect.runSync(
|
||||||
id: EventV2.ID.create(),
|
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||||
type: "session.next.compaction.delta",
|
id: EventV2.ID.create(),
|
||||||
data: {
|
type: "session.next.compaction.delta",
|
||||||
sessionID,
|
data: {
|
||||||
timestamp: DateTime.makeUnsafe(3),
|
sessionID,
|
||||||
text: "summary",
|
timestamp: DateTime.makeUnsafe(3),
|
||||||
},
|
text: "summary",
|
||||||
} satisfies SessionEvent.Event))
|
},
|
||||||
|
} satisfies SessionEvent.Event),
|
||||||
|
)
|
||||||
|
|
||||||
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
Effect.runSync(
|
||||||
id: EventV2.ID.create(),
|
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||||
type: "session.next.compaction.ended",
|
id: EventV2.ID.create(),
|
||||||
data: {
|
type: "session.next.compaction.ended",
|
||||||
sessionID,
|
data: {
|
||||||
timestamp: DateTime.makeUnsafe(4),
|
sessionID,
|
||||||
text: "final summary",
|
timestamp: DateTime.makeUnsafe(4),
|
||||||
include: "recent context",
|
text: "final summary",
|
||||||
},
|
include: "recent context",
|
||||||
} satisfies SessionEvent.Event))
|
},
|
||||||
|
} satisfies SessionEvent.Event),
|
||||||
|
)
|
||||||
|
|
||||||
expect(state.messages).toHaveLength(1)
|
expect(state.messages).toHaveLength(1)
|
||||||
expect(state.messages[0]).toMatchObject({
|
expect(state.messages[0]).toMatchObject({
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,10 @@ import type {
|
||||||
ConfigUpdateErrors,
|
ConfigUpdateErrors,
|
||||||
ConfigUpdateResponses,
|
ConfigUpdateResponses,
|
||||||
EventSubscribeResponses,
|
EventSubscribeResponses,
|
||||||
EventTuiCommandExecute2,
|
EventTuiCommandExecute,
|
||||||
EventTuiPromptAppend2,
|
EventTuiPromptAppend,
|
||||||
EventTuiSessionSelect2,
|
EventTuiSessionSelect,
|
||||||
EventTuiToastShow2,
|
EventTuiToastShow,
|
||||||
ExperimentalConsoleGetErrors,
|
ExperimentalConsoleGetErrors,
|
||||||
ExperimentalConsoleGetResponses,
|
ExperimentalConsoleGetResponses,
|
||||||
ExperimentalConsoleListOrgsErrors,
|
ExperimentalConsoleListOrgsErrors,
|
||||||
|
|
@ -4950,7 +4950,7 @@ export class Tui extends HeyApiClient {
|
||||||
parameters?: {
|
parameters?: {
|
||||||
directory?: string
|
directory?: string
|
||||||
workspace?: string
|
workspace?: string
|
||||||
body?: EventTuiPromptAppend2 | EventTuiCommandExecute2 | EventTuiToastShow2 | EventTuiSessionSelect2
|
body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect
|
||||||
},
|
},
|
||||||
options?: Options<never, ThrowOnError>,
|
options?: Options<never, ThrowOnError>,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue