mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
Merge origin/master into codex/investigate-native-sandbox-interruption
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
382318aa66
345 files changed
+2906
-1635
No files matched your search
+14
-4
@@ -762,17 +762,27 @@ path enforces a project maximum.
|
||||
and a partial unique index allows one unsettled attempt per run. The row keeps
|
||||
the context revision, the nullable primary project as provenance (no foreign
|
||||
key) and the settlement identity, which is set together with `settled_at`.
|
||||
`admitted_project_ids` is the complete admitted project set, sorted, kept
|
||||
after settlement and immutable (a trigger rejects any change). It has no
|
||||
project foreign key, so settled history never blocks project deletion and
|
||||
keeps the deleted project's ID. `{}` is a known empty set. Null means
|
||||
unknown and is allowed only on settled rows: migration `0428` filled it from
|
||||
the charges of unsettled rows, but settled `0426` rows had already lost
|
||||
their set. A retained provider session resumes with the previous set ∪ the
|
||||
current references; an unknown previous set requires a fresh context, never
|
||||
an empty or primary-only guess.
|
||||
- `execution_admission_project_charges` stores one row per distinct admitted
|
||||
project, including unlimited projects. An admission without projects has no
|
||||
rows. Composite `(company_id, …)` foreign keys keep admissions and projects in
|
||||
one company. Rows exist only while the admission is unsettled: settlement
|
||||
deletes the whole set in one transaction, and `(company_id, project_id)` is
|
||||
indexed for active counts, so lookups scale with active work, not history.
|
||||
one company. Rows are the only active capacity and exist only while the
|
||||
admission is unsettled: settlement deletes the whole set in one transaction,
|
||||
and `(company_id, project_id)` is indexed for active counts, so lookups
|
||||
scale with active work, not history.
|
||||
|
||||
Archiving a project changes no charge. Deleting a project with a charge returns
|
||||
`409 project_execution_admission_pending`; the foreign key is the backstop.
|
||||
Deleting an agent refuses unsettled admissions on its runs and removes settled
|
||||
ones. Company deletion removes the company's admissions before its runs and
|
||||
ones, with their sets, together with the runs. Company deletion removes the company's admissions before its runs and
|
||||
projects.
|
||||
|
||||
The proposed global lock order for the admission transaction is: company row
|
||||
|
||||
+8
-1
@@ -1186,7 +1186,8 @@ process per layer. Each layer reads the earlier layers as declarations:
|
||||
| `below` | Files the largest import cycle depends on, and unrelated files |
|
||||
| `cycle` | The largest import cycle |
|
||||
| `above` | Files that depend on the cycle |
|
||||
| `tests` | Test files |
|
||||
| `tests` | Test files outside `src/__tests__` |
|
||||
| `test-tree-0` to `test-tree-2` | `src/__tests__`, in shards |
|
||||
|
||||
The generated configs and declarations go in
|
||||
`server/node_modules/.cache/paperclip-typecheck`. You do not maintain a layer list.
|
||||
@@ -1195,6 +1196,12 @@ The server typecheck builds only the Runner's TypeScript output. Use
|
||||
`pnpm --filter @paperclipai/paperclip-runner build` when a local check needs the
|
||||
Runner binary.
|
||||
|
||||
`src/__tests__/tsconfig.json` holds the options for the test-tree layers, so an
|
||||
editor and the typecheck report the same errors. Server tests reach Daytona plugin
|
||||
source, but the root install does not install that plugin's dependencies. So this
|
||||
config maps `@daytonaio/sdk` to `server/scripts/typecheck-stubs/daytona-sdk.d.ts`.
|
||||
The plugin's own project checks that source against the real package.
|
||||
|
||||
If the memory available to the task is limited, use this command:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -24,6 +24,10 @@ Creation, installation, message waiting, and success use the same production
|
||||
components and server evidence as the own-app flow. A click or OAuth return does
|
||||
not constitute successful configuration.
|
||||
|
||||
Reopening a completed Slack setup retains its Slack handoff. **Done** opens the
|
||||
saved connection settings without repeating activation or reactivating a paused
|
||||
bot. Unfinished setups still complete their normal server verification first.
|
||||
|
||||
## Cloud deployment prerequisites
|
||||
|
||||
Managed setup is disabled by default. Qualification requires all of:
|
||||
|
||||
@@ -5,9 +5,11 @@ import { describe, expect, it } from "vitest";
|
||||
import { applyPendingMigrations, inspectMigrations } from "./client.js";
|
||||
import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS, getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./test-embedded-postgres.js";
|
||||
|
||||
const hashMigration = async (name: string) => createHash("sha256")
|
||||
.update(await readFile(new URL(`./migrations/${name}`, import.meta.url), "utf8")).digest("hex");
|
||||
const migrationName = "0426_absent_guardian.sql";
|
||||
const migrationHash = createHash("sha256")
|
||||
.update(await readFile(new URL(`./migrations/${migrationName}`, import.meta.url), "utf8")).digest("hex");
|
||||
const migrationHash = await hashMigration(migrationName);
|
||||
const retainedSetMigration = "0428_retained_admitted_project_sets.sql";
|
||||
const support = await getEmbeddedPostgresTestSupport();
|
||||
const describePostgres = support.supported ? describe : describe.skip;
|
||||
|
||||
@@ -17,7 +19,8 @@ describePostgres("execution admission migration", () => {
|
||||
const sql = postgres(database.connectionString, { max: 1, onnotice: () => {} });
|
||||
try {
|
||||
await sql`DROP TABLE execution_admission_project_charges, execution_admissions, project_task_execution_policies`;
|
||||
await sql`DELETE FROM drizzle.__drizzle_migrations WHERE hash = ${migrationHash}`;
|
||||
await sql`DROP FUNCTION paperclip_execution_admission_project_set_immutable`;
|
||||
await sql`DELETE FROM drizzle.__drizzle_migrations WHERE hash IN (${migrationHash}, ${await hashMigration(retainedSetMigration)})`;
|
||||
const companyId = randomUUID(), agentId = randomUUID(), projectId = randomUUID();
|
||||
await sql`INSERT INTO companies (id, name, issue_prefix) VALUES (${companyId}, 'Upgrade', 'UPG')`;
|
||||
await sql`INSERT INTO agents (id, company_id, name, adapter_type) VALUES (${agentId}, ${companyId}, 'Agent', 'process')`;
|
||||
@@ -31,7 +34,7 @@ describePostgres("execution admission migration", () => {
|
||||
});
|
||||
const before = await contents();
|
||||
expect(await inspectMigrations(database.connectionString))
|
||||
.toMatchObject({ status: "needsMigrations", pendingMigrations: [migrationName] });
|
||||
.toMatchObject({ status: "needsMigrations", pendingMigrations: [migrationName, retainedSetMigration] });
|
||||
|
||||
await applyPendingMigrations(database.connectionString);
|
||||
|
||||
@@ -50,4 +53,49 @@ describePostgres("execution admission migration", () => {
|
||||
await database.cleanup();
|
||||
}
|
||||
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
|
||||
|
||||
it("keeps the admitted sets of shipped 0426 rows that still have charges and marks settled ones unknown", async () => {
|
||||
const database = await startEmbeddedPostgresTestDatabase("paperclip-execution-admission-sets-");
|
||||
const sql = postgres(database.connectionString, { max: 1, onnotice: () => {} });
|
||||
try {
|
||||
await sql`DROP TRIGGER paperclip_execution_admission_project_set_immutable ON execution_admissions`;
|
||||
await sql`DROP FUNCTION paperclip_execution_admission_project_set_immutable`;
|
||||
await sql`ALTER TABLE execution_admissions DROP CONSTRAINT execution_admissions_project_set_check, DROP COLUMN admitted_project_ids`;
|
||||
await sql`DELETE FROM drizzle.__drizzle_migrations WHERE hash = ${await hashMigration(retainedSetMigration)}`;
|
||||
const companyId = randomUUID(), agentId = randomUUID();
|
||||
const [a, b] = [randomUUID(), randomUUID()].sort();
|
||||
await sql`INSERT INTO companies (id, name, issue_prefix) VALUES (${companyId}, 'Upgrade', 'UPS')`;
|
||||
await sql`INSERT INTO agents (id, company_id, name, adapter_type) VALUES (${agentId}, ${companyId}, 'Agent', 'process')`;
|
||||
await sql`INSERT INTO projects (id, company_id, name) VALUES (${b}, ${companyId}, 'B'), (${a}, ${companyId}, 'A')`;
|
||||
const [live, empty, settled] = [randomUUID(), randomUUID(), randomUUID()];
|
||||
for (const id of [live, empty, settled]) {
|
||||
const [run] = await sql`INSERT INTO heartbeat_runs (company_id, agent_id) VALUES (${companyId}, ${agentId}) RETURNING id`;
|
||||
await sql`INSERT INTO execution_admissions (id, company_id, run_id, attempt, primary_project_id, context_revision,
|
||||
settlement_id, settled_at) VALUES (${id}, ${companyId}, ${run.id}, 1, ${a}, 'issue:1',
|
||||
${id === settled ? randomUUID() : null}, ${id === settled ? sql`now()` : null})`;
|
||||
}
|
||||
await sql`INSERT INTO execution_admission_project_charges (admission_id, company_id, project_id)
|
||||
VALUES (${live}, ${companyId}, ${b}), (${live}, ${companyId}, ${a})`;
|
||||
|
||||
const sets = () => sql`SELECT id, admitted_project_ids FROM execution_admissions ORDER BY id`;
|
||||
await applyPendingMigrations(database.connectionString);
|
||||
|
||||
const upgraded = await sets();
|
||||
expect(upgraded).toEqual(expect.arrayContaining([
|
||||
{ id: live, admitted_project_ids: [a, b] },
|
||||
{ id: empty, admitted_project_ids: [] },
|
||||
{ id: settled, admitted_project_ids: null },
|
||||
]));
|
||||
await sql`DELETE FROM drizzle.__drizzle_migrations WHERE hash = ${await hashMigration(retainedSetMigration)}`;
|
||||
await applyPendingMigrations(database.connectionString);
|
||||
expect(await sets()).toEqual(upgraded);
|
||||
expect(await sql`SELECT count(*)::int AS count FROM execution_admission_project_charges`).toEqual([{ count: 2 }]);
|
||||
await expect(sql`UPDATE execution_admissions SET admitted_project_ids = '{}' WHERE id = ${settled}`)
|
||||
.rejects.toMatchObject({ message: "execution admission project set is immutable" });
|
||||
expect(await inspectMigrations(database.connectionString)).toMatchObject({ status: "upToDate" });
|
||||
} finally {
|
||||
await sql.end();
|
||||
await database.cleanup();
|
||||
}
|
||||
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
ALTER TABLE "execution_admissions" ADD COLUMN IF NOT EXISTS "admitted_project_ids" uuid[];--> statement-breakpoint
|
||||
-- Unsettled charges are still the complete admitted set. Settled rows already lost theirs and stay unknown (null).
|
||||
UPDATE "execution_admissions" AS "admission" SET "admitted_project_ids" = coalesce((
|
||||
SELECT array_agg("charge"."project_id" ORDER BY "charge"."project_id") FROM "execution_admission_project_charges" AS "charge"
|
||||
WHERE "charge"."company_id" = "admission"."company_id" AND "charge"."admission_id" = "admission"."id"
|
||||
), '{}'::uuid[]) WHERE "admission"."settled_at" IS NULL AND "admission"."admitted_project_ids" IS NULL;--> statement-breakpoint
|
||||
ALTER TABLE "execution_admissions" DROP CONSTRAINT IF EXISTS "execution_admissions_project_set_check";--> statement-breakpoint
|
||||
ALTER TABLE "execution_admissions" ADD CONSTRAINT "execution_admissions_project_set_check" CHECK (case when "execution_admissions"."admitted_project_ids" is null
|
||||
then "execution_admissions"."settled_at" is not null else array_position("execution_admissions"."admitted_project_ids", null) is null and cardinality("execution_admissions"."admitted_project_ids") <= 100 end);--> statement-breakpoint
|
||||
CREATE OR REPLACE FUNCTION paperclip_execution_admission_project_set_immutable() RETURNS trigger
|
||||
LANGUAGE plpgsql SET search_path = public, pg_temp AS $$
|
||||
BEGIN
|
||||
IF NEW.admitted_project_ids IS DISTINCT FROM OLD.admitted_project_ids THEN
|
||||
RAISE EXCEPTION 'execution admission project set is immutable' USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
--> statement-breakpoint
|
||||
DROP TRIGGER IF EXISTS paperclip_execution_admission_project_set_immutable ON "execution_admissions";--> statement-breakpoint
|
||||
CREATE TRIGGER paperclip_execution_admission_project_set_immutable BEFORE UPDATE OF admitted_project_ids ON "execution_admissions"
|
||||
FOR EACH ROW EXECUTE FUNCTION paperclip_execution_admission_project_set_immutable();
|
||||
+361
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "fe0b877c-d63e-46d9-8798-c7729fd13eb7",
|
||||
"prevId": "deb3f1b9-6eb7-447f-adf2-dee69f943c59",
|
||||
"id": "fb4383f6-cbe6-4aff-9fd2-6eeb69a6927a",
|
||||
"prevId": "dd860bff-55de-4849-8787-58b9de729258",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
@@ -28825,6 +28825,337 @@
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.execution_admission_project_charges": {
|
||||
"name": "execution_admission_project_charges",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"admission_id": {
|
||||
"name": "admission_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"execution_admission_project_charges_project_idx": {
|
||||
"name": "execution_admission_project_charges_project_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "project_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"execution_admission_project_charges_company_admission_fk": {
|
||||
"name": "execution_admission_project_charges_company_admission_fk",
|
||||
"tableFrom": "execution_admission_project_charges",
|
||||
"tableTo": "execution_admissions",
|
||||
"columnsFrom": [
|
||||
"company_id",
|
||||
"admission_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"company_id",
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"execution_admission_project_charges_company_project_fk": {
|
||||
"name": "execution_admission_project_charges_company_project_fk",
|
||||
"tableFrom": "execution_admission_project_charges",
|
||||
"tableTo": "projects",
|
||||
"columnsFrom": [
|
||||
"company_id",
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"company_id",
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"execution_admission_project_charges_admission_id_project_id_pk": {
|
||||
"name": "execution_admission_project_charges_admission_id_project_id_pk",
|
||||
"columns": [
|
||||
"admission_id",
|
||||
"project_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.execution_admissions": {
|
||||
"name": "execution_admissions",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"run_id": {
|
||||
"name": "run_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"attempt": {
|
||||
"name": "attempt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"primary_project_id": {
|
||||
"name": "primary_project_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"context_revision": {
|
||||
"name": "context_revision",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"admitted_project_ids": {
|
||||
"name": "admitted_project_ids",
|
||||
"type": "uuid[]",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"settlement_id": {
|
||||
"name": "settlement_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"settled_at": {
|
||||
"name": "settled_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"execution_admissions_company_id_uq": {
|
||||
"name": "execution_admissions_company_id_uq",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"execution_admissions_run_attempt_uq": {
|
||||
"name": "execution_admissions_run_attempt_uq",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "run_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "attempt",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"execution_admissions_live_run_uq": {
|
||||
"name": "execution_admissions_live_run_uq",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "run_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"where": "\"execution_admissions\".\"settled_at\" is null",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"execution_admissions_company_id_companies_id_fk": {
|
||||
"name": "execution_admissions_company_id_companies_id_fk",
|
||||
"tableFrom": "execution_admissions",
|
||||
"tableTo": "companies",
|
||||
"columnsFrom": [
|
||||
"company_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"execution_admissions_run_id_heartbeat_runs_id_fk": {
|
||||
"name": "execution_admissions_run_id_heartbeat_runs_id_fk",
|
||||
"tableFrom": "execution_admissions",
|
||||
"tableTo": "heartbeat_runs",
|
||||
"columnsFrom": [
|
||||
"run_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {
|
||||
"execution_admissions_attempt_check": {
|
||||
"name": "execution_admissions_attempt_check",
|
||||
"value": "\"execution_admissions\".\"attempt\" > 0"
|
||||
},
|
||||
"execution_admissions_settlement_check": {
|
||||
"name": "execution_admissions_settlement_check",
|
||||
"value": "(\"execution_admissions\".\"settlement_id\" is null) = (\"execution_admissions\".\"settled_at\" is null)"
|
||||
},
|
||||
"execution_admissions_project_set_check": {
|
||||
"name": "execution_admissions_project_set_check",
|
||||
"value": "case when \"execution_admissions\".\"admitted_project_ids\" is null\n then \"execution_admissions\".\"settled_at\" is not null else array_position(\"execution_admissions\".\"admitted_project_ids\", null) is null and cardinality(\"execution_admissions\".\"admitted_project_ids\") <= 100 end"
|
||||
}
|
||||
},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.project_task_execution_policies": {
|
||||
"name": "project_task_execution_policies",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"max_concurrent_tasks": {
|
||||
"name": "max_concurrent_tasks",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"project_task_execution_policies_company_project_fk": {
|
||||
"name": "project_task_execution_policies_company_project_fk",
|
||||
"tableFrom": "project_task_execution_policies",
|
||||
"tableTo": "projects",
|
||||
"columnsFrom": [
|
||||
"company_id",
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"company_id",
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {
|
||||
"project_task_execution_policies_maximum_check": {
|
||||
"name": "project_task_execution_policies_maximum_check",
|
||||
"value": "\"project_task_execution_policies\".\"max_concurrent_tasks\" is null or \"project_task_execution_policies\".\"max_concurrent_tasks\" > 0"
|
||||
}
|
||||
},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.execution_workspace_repositories": {
|
||||
"name": "execution_workspace_repositories",
|
||||
"schema": "",
|
||||
@@ -29999,6 +30330,22 @@
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"execution_workspace_snapshots_imported_children_idx": {
|
||||
"name": "execution_workspace_snapshots_imported_children_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "(\"metadata\"->'importedWorkspaceChildren') jsonb_path_ops",
|
||||
"asc": true,
|
||||
"isExpression": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": "\"execution_workspace_snapshots\".\"metadata\" ? 'importedWorkspaceChildren'",
|
||||
"concurrently": false,
|
||||
"method": "gin",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
@@ -50480,6 +50827,12 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_by_run_id": {
|
||||
"name": "created_by_run_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"dispatch_fingerprint": {
|
||||
"name": "dispatch_fingerprint",
|
||||
"type": "text",
|
||||
@@ -62442,6 +62795,12 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cleanup_verified_at": {
|
||||
"name": "cleanup_verified_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
+7
@@ -2983,6 +2983,13 @@
|
||||
"when": 1793736361959,
|
||||
"tag": "0427_ordinary_xavin",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 428,
|
||||
"version": "7",
|
||||
"when": 1793743828770,
|
||||
"tag": "0428_retained_admitted_project_sets",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -24,7 +24,10 @@ export const projectTaskExecutionPolicies = pgTable("project_task_execution_poli
|
||||
/**
|
||||
* One admission attempt of a run. A run can be admitted again after a settled
|
||||
* attempt, so the run ID is not the admission identity. Primary project is
|
||||
* provenance only; the charges below are the admitted project set.
|
||||
* provenance only. `admittedProjectIds` is the complete admitted set, sorted and
|
||||
* immutable (trigger), kept after settlement without project foreign keys. Null
|
||||
* means unknown: settled before the set was recorded. Charges below are the
|
||||
* active capacity only.
|
||||
*/
|
||||
export const executionAdmissions = pgTable("execution_admissions", {
|
||||
id: uuid("id").primaryKey(),
|
||||
@@ -33,6 +36,7 @@ export const executionAdmissions = pgTable("execution_admissions", {
|
||||
attempt: integer("attempt").notNull(),
|
||||
primaryProjectId: uuid("primary_project_id"),
|
||||
contextRevision: text("context_revision").notNull(),
|
||||
admittedProjectIds: uuid("admitted_project_ids").array(),
|
||||
settlementId: uuid("settlement_id"),
|
||||
settledAt: timestamp("settled_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
@@ -42,6 +46,8 @@ export const executionAdmissions = pgTable("execution_admissions", {
|
||||
liveRun: uniqueIndex("execution_admissions_live_run_uq").on(t.runId).where(sql`${t.settledAt} is null`),
|
||||
attemptCheck: check("execution_admissions_attempt_check", sql`${t.attempt} > 0`),
|
||||
settlementCheck: check("execution_admissions_settlement_check", sql`(${t.settlementId} is null) = (${t.settledAt} is null)`),
|
||||
projectSetCheck: check("execution_admissions_project_set_check", sql`case when ${t.admittedProjectIds} is null
|
||||
then ${t.settledAt} is not null else array_position(${t.admittedProjectIds}, null) is null and cardinality(${t.admittedProjectIds}) <= 100 end`),
|
||||
}));
|
||||
|
||||
/** One active charge per distinct admitted project. Settlement deletes the whole set; a charged project cannot be deleted. */
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
canonicalNativeRuntimeContextDigest,
|
||||
composeNativeSystemInstructions,
|
||||
nativeRuntimePromptDigest,
|
||||
type NativeRuntimeContextSnapshot,
|
||||
} from "./runtime-context.js";
|
||||
|
||||
const input: NativeExecutionInputV1 = {
|
||||
@@ -81,6 +82,7 @@ describe("NativeExecutionInputV1", () => {
|
||||
runtimeContext: { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) },
|
||||
}));
|
||||
const recovered = parseNativeExecutionInput(persisted);
|
||||
if (!("runtimeContext" in recovered)) throw new Error("Expected a runtime context");
|
||||
expect(recovered.runtimeContext).toEqual(persisted.runtimeContext);
|
||||
expect(parseNativeExecutionInput(recovered)).toEqual(recovered);
|
||||
},
|
||||
@@ -88,7 +90,7 @@ describe("NativeExecutionInputV1", () => {
|
||||
|
||||
it("parses v3 immutable runtime context without changing the model task envelope", () => {
|
||||
const digest = "0".repeat(64);
|
||||
const context = {
|
||||
const context: Omit<NativeRuntimeContextSnapshot, "aggregateDigest"> = {
|
||||
prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
|
||||
instructions: {
|
||||
entryPath: "AGENTS.md",
|
||||
@@ -101,7 +103,7 @@ describe("NativeExecutionInputV1", () => {
|
||||
bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/skills/research", fileCount: 2, totalBytes: 42 },
|
||||
}],
|
||||
mcp: { assignmentSetId: "sha256:test", digest, bindingId: "native-mcp:run-1" },
|
||||
} as const;
|
||||
};
|
||||
const preconfigured = {
|
||||
...input, schema: "paperclip.native-execution-input.v7", homeMode: "preconfigured",
|
||||
provider: { kind: "codex", model: null, approvalPolicy: "never" },
|
||||
@@ -129,6 +131,7 @@ describe("NativeExecutionInputV1", () => {
|
||||
runtimeContext: { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) },
|
||||
});
|
||||
expect(parsed.schema).toBe("paperclip.native-execution-input.v3");
|
||||
if (!("runtimeContext" in parsed)) throw new Error("Expected a runtime context");
|
||||
const envelope = buildNativeModelEnvelope(parsed);
|
||||
expect(envelope.task).toEqual(buildNativeModelEnvelope(input).task);
|
||||
expect(envelope.completionContract).toEqual(buildNativeModelEnvelope(input).completionContract);
|
||||
@@ -277,7 +280,7 @@ describe("NativeExecutionInputV1", () => {
|
||||
});
|
||||
|
||||
it("deserializes pre-provider Codex state as Codex", () => {
|
||||
const legacy = structuredClone(input) as Record<string, unknown>;
|
||||
const legacy: Record<string, unknown> = { ...structuredClone(input) };
|
||||
delete legacy.provider;
|
||||
expect(parseNativeExecutionInput(legacy).provider).toEqual({
|
||||
kind: "codex",
|
||||
@@ -448,9 +451,7 @@ describe("NativeExecutionInputV1", () => {
|
||||
});
|
||||
|
||||
it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40] as const)("decodes closed Copilot profile history at version %s without admitting a future version", (agentProfileVersion) => {
|
||||
const { qualificationModel: _qualificationModel, reportedModelId: _reportedModelId,
|
||||
permissionPolicy, qualificationStatus: _qualificationStatus, modelPolicy: _modelPolicy,
|
||||
...snapshot } = QUALIFIED_ACPX_PROFILES.copilot;
|
||||
const { permissionPolicy, qualificationStatus: _qualificationStatus, ...snapshot } = QUALIFIED_ACPX_PROFILES.copilot;
|
||||
const provider = {
|
||||
kind: "acpx", agent: "copilot", model: "gpt-5.6-luna", permissionPolicy,
|
||||
profile: { ...snapshot, agentProfileVersion },
|
||||
@@ -474,11 +475,10 @@ describe("NativeExecutionInputV1", () => {
|
||||
it.each(Object.values(QUALIFIED_ACPX_PROFILES))(
|
||||
"admits the current $agent profile through the native execution boundary",
|
||||
(declaration) => {
|
||||
const { qualificationModel, reportedModelId: _reported, permissionPolicy,
|
||||
modelPolicy: _modelPolicy, qualificationStatus: _status, ...profile } = declaration;
|
||||
const { permissionPolicy, qualificationStatus: _status, ...profile } = declaration;
|
||||
const provider = {
|
||||
kind: "acpx", agent: declaration.agent,
|
||||
model: qualificationModel || "explicit-provider-model",
|
||||
model: "explicit-provider-model",
|
||||
permissionPolicy, profile,
|
||||
...(declaration.agent === "pi" ? { piThinkingLevel: "low" } : {}),
|
||||
};
|
||||
@@ -493,7 +493,7 @@ describe("NativeExecutionInputV1", () => {
|
||||
);
|
||||
|
||||
it("defaults legacy lifecycle state to per-turn and validates warm timeouts", () => {
|
||||
const legacy = structuredClone(input) as Record<string, unknown>;
|
||||
const legacy: Record<string, unknown> = { ...structuredClone(input) };
|
||||
delete (legacy.session as Record<string, unknown>).lifecyclePolicy;
|
||||
expect(parseNativeExecutionInput(legacy).session.lifecyclePolicy).toEqual({
|
||||
mode: "per_turn",
|
||||
@@ -566,11 +566,11 @@ describe("NativeExecutionInputV2 planning", () => {
|
||||
})).toMatchObject({ task: { workMode: "planning" }, executionMode: "default" });
|
||||
});
|
||||
|
||||
const cloudContext = {
|
||||
const cloudContext: Omit<NativeRuntimeContextSnapshot, "aggregateDigest"> = {
|
||||
prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
|
||||
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest: "0".repeat(64), manifestDigest: "0".repeat(64), rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } },
|
||||
skills: [], mcp: { assignmentSetId: "none", digest: "0".repeat(64), bindingId: null },
|
||||
} as const;
|
||||
};
|
||||
const cloudPlanning = {
|
||||
...planning, schema: "paperclip.native-execution-input.v5",
|
||||
provider: { kind: "cursor_cloud", model: "explicit-cloud-model", cursorCloud: { schema: "paperclip.cursor-cloud.config.v1", repos: [{ url: "https://github.com/example/fixture" }] } },
|
||||
@@ -639,12 +639,12 @@ describe.each(["ask", "skill_test"])("NativeExecutionInputV2 %s mode", (workMode
|
||||
describe("native task context ownership", () => {
|
||||
function currentInput() {
|
||||
const digest = "0".repeat(64);
|
||||
const context = {
|
||||
const context: Omit<NativeRuntimeContextSnapshot, "aggregateDigest"> = {
|
||||
prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
|
||||
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } },
|
||||
skills: [],
|
||||
mcp: { assignmentSetId: "none", digest, bindingId: null },
|
||||
} as const;
|
||||
};
|
||||
return parseNativeExecutionInput({
|
||||
...input,
|
||||
schema: NATIVE_EXECUTION_INPUT_SCHEMA,
|
||||
@@ -656,8 +656,7 @@ describe("native task context ownership", () => {
|
||||
});
|
||||
}
|
||||
|
||||
const { qualificationModel: _copilotQualificationModel, reportedModelId: _copilotReportedModelId,
|
||||
permissionPolicy: _copilotPermissionPolicy, qualificationStatus: _copilotQualificationStatus, modelPolicy: _copilotModelPolicy,
|
||||
const { permissionPolicy: _copilotPermissionPolicy, qualificationStatus: _copilotQualificationStatus,
|
||||
...copilotSnapshot } = QUALIFIED_ACPX_PROFILES.copilot;
|
||||
|
||||
it.each([
|
||||
@@ -690,6 +689,7 @@ describe("native task context ownership", () => {
|
||||
} : {}),
|
||||
}));
|
||||
const recovered = parseNativeExecutionInput(persisted);
|
||||
if (!("runtimeContext" in recovered)) throw new Error("Expected a runtime context");
|
||||
expect(recovered.runtimeContext).toEqual(context);
|
||||
expect(recovered.provider).toEqual(provider);
|
||||
expect(recovered.session.driverKind).toBe(driverKind);
|
||||
@@ -759,6 +759,7 @@ describe("native task context ownership", () => {
|
||||
|
||||
const fullResume = buildNativeModelEnvelope(parsed, { resumedSession: true });
|
||||
expect(fullResume.schema).toBe(envelopeSchema);
|
||||
if (!("task" in fullResume)) throw new Error("Expected a full resume envelope");
|
||||
expect(fullResume.task.prompt).toBe(parsed.task.prompt);
|
||||
expect(fullResume.task.prompt).not.toContain(guidance);
|
||||
|
||||
@@ -796,6 +797,7 @@ describe("native task context ownership", () => {
|
||||
completionContract: { ...execution.completionContract, contract },
|
||||
completionSources: { promptSha256: createHash("sha256").update(execution.task.prompt).digest("hex"), contractRevision: contract.revision, criteria: [{ id: "objective", source }] },
|
||||
});
|
||||
if (completeInput.schema !== NATIVE_EXECUTION_INPUT_SCHEMA) throw new Error("Expected a v5 execution input");
|
||||
const before = structuredClone(completeInput.completionContract);
|
||||
const envelope = buildNativeModelEnvelope(completeInput);
|
||||
expect(envelope.completionContract.criteria).toEqual([
|
||||
@@ -815,7 +817,7 @@ describe("native task context ownership", () => {
|
||||
|
||||
|
||||
describe("Claude effort input v9", () => {
|
||||
const { permissionPolicy: _permission, qualificationModel: _model, reportedModelId: _reported, ...profile } = QUALIFIED_ACPX_PROFILES.claude;
|
||||
const { permissionPolicy: _permission, ...profile } = QUALIFIED_ACPX_PROFILES.claude;
|
||||
const digest = "0".repeat(64);
|
||||
const context = { prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
|
||||
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } },
|
||||
@@ -856,7 +858,7 @@ describe("Claude effort input v9", () => {
|
||||
});
|
||||
|
||||
describe("Grok effort input v10", () => {
|
||||
const { permissionPolicy: _permission, qualificationModel: _model, reportedModelId: _reported, ...profile } = QUALIFIED_ACPX_PROFILES.grok;
|
||||
const { permissionPolicy: _permission, ...profile } = QUALIFIED_ACPX_PROFILES.grok;
|
||||
const digest = "0".repeat(64);
|
||||
const context = { prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
|
||||
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } },
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// cycle the largest import cycle, reading `below` as declarations
|
||||
// above files that depend on the cycle, reading both as declarations
|
||||
// tests test files, reading all three as declarations
|
||||
// test-tree-N src/__tests__, in shards, reading the source layers as declarations
|
||||
//
|
||||
// The layers come from the import graph on every run, so nothing has to move
|
||||
// and no list can go stale. The import scan does not have to be perfect: if it
|
||||
@@ -22,21 +23,26 @@
|
||||
//
|
||||
// Projects outside the server read the source layers as declarations through a
|
||||
// `tsconfig.layers.json` that references tsconfig.declarations.json in the output
|
||||
// directory. --declarations-only builds those layers and skips the tests layer.
|
||||
// directory. --declarations-only builds those layers and skips the test layers.
|
||||
//
|
||||
// Usage: node scripts/typecheck-layers.mjs [--declarations-only]
|
||||
|
||||
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { crc32 } from "node:zlib";
|
||||
import { runTsc } from "../../scripts/run-tsc.mjs";
|
||||
|
||||
const IMPORT_SPECIFIER = /(?:from\s+|import\s*\(\s*|import\s+)["'](\.[^"']+)["']/g;
|
||||
const OUTPUT_DIRECTORY = path.join("node_modules", ".cache", "paperclip-typecheck");
|
||||
// One program over this tree peaks at 3.9 GiB on one thread. Three peak at 2.5 GiB.
|
||||
const TEST_TREE_SHARDS = 3;
|
||||
|
||||
export const isTestFile = (file) => /\.test\.tsx?$/.test(file) || file.split("/").includes("__tests__");
|
||||
// tsconfig.json excludes this tree. A test reaches its helpers through imports.
|
||||
const isExcludedFromProgram = (file) => file.startsWith("src/__tests__/");
|
||||
// tsconfig.json excludes this tree, so the test-tree layers check it.
|
||||
const isTestTreeFile = (file) => file.startsWith("src/__tests__/");
|
||||
// Test layers emit nothing, so other projects cannot read them as declarations.
|
||||
const emitsDeclarations = (layer) => layer.name !== "tests" && !layer.testTree;
|
||||
|
||||
export function listSourceFiles(serverRoot) {
|
||||
const files = [];
|
||||
@@ -174,7 +180,12 @@ export function planLayers(files) {
|
||||
const dependents = reachableFrom(cycle, reversed);
|
||||
const above = [...dependents].filter((file) => !cycle.has(file));
|
||||
const below = [...graph.keys()].filter((file) => !dependents.has(file));
|
||||
const tests = [...candidates.keys()].filter((file) => isTestFile(file) && !isExcludedFromProgram(file));
|
||||
const tests = [...candidates.keys()].filter((file) => isTestFile(file) && !isTestTreeFile(file));
|
||||
// A hash keeps each file in its shard when other files are added or removed.
|
||||
const testTree = Array.from({ length: TEST_TREE_SHARDS }, () => []);
|
||||
for (const file of candidates.keys()) {
|
||||
if (isTestTreeFile(file)) testTree[crc32(file) % TEST_TREE_SHARDS].push(file);
|
||||
}
|
||||
|
||||
const layer = (name, members, references) => {
|
||||
const sorted = [...members].sort();
|
||||
@@ -186,6 +197,10 @@ export function planLayers(files) {
|
||||
layer("cycle", cycle, ["below"]),
|
||||
layer("above", above, ["below", "cycle"]),
|
||||
layer("tests", tests, ["below", "cycle", "above"]),
|
||||
...testTree.map((shard, index) => ({
|
||||
...layer(`test-tree-${index}`, shard, ["below", "cycle", "above"]),
|
||||
testTree: true,
|
||||
})),
|
||||
].filter((candidate) => candidate.sourceFiles.length > 0);
|
||||
}
|
||||
|
||||
@@ -195,6 +210,15 @@ export function layerConfig(layer, { emptyLayers = new Set() } = {}) {
|
||||
const references = layer.references
|
||||
.filter((name) => !emptyLayers.has(name))
|
||||
.map((name) => ({ path: `./tsconfig.${name}.json` }));
|
||||
// Editors read the same options from this file.
|
||||
if (layer.testTree) {
|
||||
return {
|
||||
extends: fromOutput("src/__tests__/tsconfig.json"),
|
||||
files: layer.files.map(fromOutput),
|
||||
include: [],
|
||||
references,
|
||||
};
|
||||
}
|
||||
const output =
|
||||
layer.name === "tests"
|
||||
? { composite: false, declaration: false, declarationMap: false, sourceMap: false, noEmit: true }
|
||||
@@ -222,7 +246,7 @@ export function declarationsConfig(layers) {
|
||||
compilerOptions: { composite: true },
|
||||
files: [],
|
||||
references: layers
|
||||
.filter((layer) => layer.name !== "tests")
|
||||
.filter(emitsDeclarations)
|
||||
.map((layer) => ({ path: `./tsconfig.${layer.name}.json` })),
|
||||
};
|
||||
}
|
||||
@@ -259,7 +283,7 @@ export function typecheckLayers({
|
||||
|
||||
let exitCode = 0;
|
||||
for (const layer of layers) {
|
||||
if (declarationsOnly && layer.name === "tests") continue;
|
||||
if (declarationsOnly && !emitsDeclarations(layer)) continue;
|
||||
log(`typecheck:layers: ${layer.name} (${layer.sourceFiles.length} files)`);
|
||||
const config = path.join(OUTPUT_DIRECTORY, `tsconfig.${layer.name}.json`);
|
||||
const { status, signal } = run(["-p", config], { cwd: serverRoot });
|
||||
|
||||
@@ -76,7 +76,7 @@ describe("planLayers", () => {
|
||||
for (const layer of layers) expect(layer.files).toContain("src/types/ambient.d.ts");
|
||||
});
|
||||
|
||||
it("keeps test support files in the test layer and leaves src/__tests__ to imports", () => {
|
||||
it("keeps test support files in the test layer and shards src/__tests__ by a stable hash", () => {
|
||||
const layers = planLayers(
|
||||
new Map([
|
||||
["src/a.ts", ""],
|
||||
@@ -88,6 +88,25 @@ describe("planLayers", () => {
|
||||
|
||||
expect(sourcesOf(layers, "below")).toEqual(["src/a.ts"]);
|
||||
expect(sourcesOf(layers, "tests")).toEqual(["src/services/__tests__/fixtures.ts"]);
|
||||
const shards = layers.filter((layer) => layer.testTree);
|
||||
expect(shards.flatMap((layer) => layer.sourceFiles).sort()).toEqual([
|
||||
"src/__tests__/big.test.ts",
|
||||
"src/__tests__/helpers/db.ts",
|
||||
]);
|
||||
for (const shard of shards) expect(shard.references).toEqual(["below", "cycle", "above"]);
|
||||
|
||||
const withMore = planLayers(
|
||||
new Map([
|
||||
["src/a.ts", ""],
|
||||
["src/__tests__/helpers/db.ts", ""],
|
||||
["src/__tests__/big.test.ts", ""],
|
||||
["src/__tests__/added-1.test.ts", ""],
|
||||
["src/__tests__/added-2.test.ts", ""],
|
||||
]),
|
||||
);
|
||||
for (const shard of shards) {
|
||||
for (const file of shard.sourceFiles) expect(sourcesOf(withMore, shard.name)).toContain(file);
|
||||
}
|
||||
});
|
||||
|
||||
it("lists imported JSON in the layer that imports it", () => {
|
||||
@@ -120,14 +139,14 @@ describe("planLayers", () => {
|
||||
const planned = layers.flatMap((layer) => layer.sourceFiles);
|
||||
expect(new Set(planned).size).toBe(planned.length);
|
||||
expect(planned.sort()).toEqual(
|
||||
sourceFiles.filter((file) => !file.endsWith(".d.ts") && !file.startsWith("src/__tests__/")).sort(),
|
||||
sourceFiles.filter((file) => !file.endsWith(".d.ts")).sort(),
|
||||
);
|
||||
|
||||
const position = new Map(
|
||||
layers.flatMap((layer, index) => layer.sourceFiles.map((file) => [file, index])),
|
||||
);
|
||||
const imports = readImports(new Map([...files].filter(([file]) => !file.endsWith(".d.ts"))));
|
||||
for (const layer of layers.filter((candidate) => candidate.name !== "tests")) {
|
||||
for (const layer of layers.filter((candidate) => candidate.name !== "tests" && !candidate.testTree)) {
|
||||
for (const file of layer.sourceFiles) {
|
||||
for (const target of imports.get(file).modules) {
|
||||
if (position.has(target)) expect(position.get(target)).toBeLessThanOrEqual(position.get(file));
|
||||
@@ -160,9 +179,37 @@ describe("layerConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("layerConfig for the test tree", () => {
|
||||
it("reads the options that editors read and emits nothing", () => {
|
||||
const config = layerConfig({
|
||||
name: "test-tree-0",
|
||||
files: ["src/__tests__/a.test.ts"],
|
||||
references: ["below"],
|
||||
testTree: true,
|
||||
});
|
||||
|
||||
expect(config.extends).toBe("../../../src/__tests__/tsconfig.json");
|
||||
expect(config.compilerOptions).toBeUndefined();
|
||||
expect(config.files).toEqual(["../../../src/__tests__/a.test.ts"]);
|
||||
expect(config.references).toEqual([{ path: "./tsconfig.below.json" }]);
|
||||
});
|
||||
|
||||
it("has a real tests tsconfig that cannot emit and sees the ambient types", () => {
|
||||
const config = JSON.parse(readFileSync(path.join(serverRoot, "src/__tests__/tsconfig.json"), "utf8"));
|
||||
|
||||
expect(config.compilerOptions).toMatchObject({ noEmit: true, rootDir: "../../.." });
|
||||
expect(config.include).toContain("../types/*.d.ts");
|
||||
});
|
||||
});
|
||||
|
||||
describe("declarationsConfig", () => {
|
||||
it("references the source layers that exist, and not tests", () => {
|
||||
const config = declarationsConfig([{ name: "below" }, { name: "above" }, { name: "tests" }]);
|
||||
it("references the source layers that exist, and no test layer", () => {
|
||||
const config = declarationsConfig([
|
||||
{ name: "below" },
|
||||
{ name: "above" },
|
||||
{ name: "tests" },
|
||||
{ name: "test-tree-0", testTree: true },
|
||||
]);
|
||||
|
||||
expect(config.compilerOptions.composite).toBe(true);
|
||||
expect(config.files).toEqual([]);
|
||||
@@ -213,8 +260,10 @@ describe("typecheckLayers", () => {
|
||||
expect(JSON.parse(readFileSync(path.join(output, "package.json"), "utf8"))).toEqual({ type: "module" });
|
||||
});
|
||||
|
||||
it("skips the tests layer when only declarations are needed", () => {
|
||||
it("skips the test layers when only declarations are needed", () => {
|
||||
const root = serverFixture();
|
||||
mkdirSync(path.join(root, "src", "__tests__"));
|
||||
writeFileSync(path.join(root, "src", "__tests__", "route.test.ts"), `import "../route.js";`);
|
||||
const configs = [];
|
||||
|
||||
const status = typecheckLayers({
|
||||
@@ -405,6 +454,53 @@ describe("typecheckLayers with the real compiler", () => {
|
||||
expect(output).toContain("leaf");
|
||||
}, 60_000);
|
||||
|
||||
function addTestTree(root, source) {
|
||||
mkdirSync(path.join(root, "src", "__tests__"), { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(root, "src", "__tests__", "tsconfig.json"),
|
||||
`${JSON.stringify({
|
||||
extends: "../../tsconfig.json",
|
||||
compilerOptions: { noEmit: true, rootDir: "../.." },
|
||||
include: ["."],
|
||||
})}\n`,
|
||||
);
|
||||
writeFileSync(path.join(root, "src", "__tests__", "tree.test.ts"), source);
|
||||
}
|
||||
|
||||
it("checks the test tree through declarations", () => {
|
||||
const root = compilerFixture();
|
||||
addTestTree(root, `import { route } from "../route.js";\nexport const checked: number = route();\n`);
|
||||
|
||||
const { status, output } = check(root);
|
||||
|
||||
expect(output).toBe("");
|
||||
expect(status).toBe(0);
|
||||
}, 60_000);
|
||||
|
||||
it("fails on a type error in the test tree", () => {
|
||||
const root = compilerFixture();
|
||||
addTestTree(root, `import { route } from "../route.js";\nexport const checked: string = route();\n`);
|
||||
|
||||
const { status, output } = check(root);
|
||||
|
||||
expect(status).not.toBe(0);
|
||||
expect(output).toMatch(/tree\.test\.ts\(2,\d+\): error TS2322/);
|
||||
}, 60_000);
|
||||
|
||||
it("fails when the test-tree tsconfig is invalid", () => {
|
||||
const root = compilerFixture();
|
||||
addTestTree(root, `export const checked = 1;\n`);
|
||||
writeFileSync(
|
||||
path.join(root, "src", "__tests__", "tsconfig.json"),
|
||||
`${JSON.stringify({ extends: "../../tsconfig.json", compilerOptions: { noEmit: true, rootDir: "../..", target: "not-a-target" } })}\n`,
|
||||
);
|
||||
|
||||
const { status, output } = check(root);
|
||||
|
||||
expect(status).not.toBe(0);
|
||||
expect(output).toMatch(/error TS\d+/);
|
||||
}, 60_000);
|
||||
|
||||
it("reports a type change through declarations in the test layer", () => {
|
||||
const root = compilerFixture();
|
||||
writeFileSync(
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Server tests reach Daytona plugin source, but the root install does not install
|
||||
// that plugin's dependencies. Its own project checks that source against the real
|
||||
// package. The server tests only need these names to resolve. Async calls return a
|
||||
// Promise because a bare `any` makes the plugin's generic helpers infer `unknown`.
|
||||
type AsyncMembers = { [member: string]: (...args: any[]) => Promise<any> };
|
||||
|
||||
export declare class Daytona {
|
||||
constructor(config?: DaytonaConfig);
|
||||
snapshot: AsyncMembers;
|
||||
get(...args: any[]): Promise<any>;
|
||||
create(...args: any[]): Promise<any>;
|
||||
list(...args: any[]): Promise<any>;
|
||||
[member: string]: any;
|
||||
}
|
||||
export declare class DaytonaNotFoundError extends Error {}
|
||||
export declare class DaytonaTimeoutError extends Error {}
|
||||
export type CreateSandboxBaseParams = any;
|
||||
export type CreateSandboxFromImageParams = any;
|
||||
export type CreateSandboxFromSnapshotParams = any;
|
||||
export type DaytonaConfig = any;
|
||||
export type FileDownloadRequest = any;
|
||||
export type FileDownloadResponse = any;
|
||||
export type FileUpload = any;
|
||||
export type Resources = any;
|
||||
export interface Sandbox {
|
||||
fs: { downloadFiles(...args: any[]): Promise<any[]>; [member: string]: (...args: any[]) => Promise<any> };
|
||||
process: {
|
||||
createPty: (...args: any[]) => Promise<any>;
|
||||
executeCommand(...args: any[]): Promise<{ result?: string; artifacts?: { stdout?: string }; exitCode?: number }>;
|
||||
getSessionCommandLogs(
|
||||
sessionId: string,
|
||||
commandId: string,
|
||||
onStdout?: (chunk: string) => void,
|
||||
onStderr?: (chunk: string) => void,
|
||||
): Promise<any>;
|
||||
[member: string]: (...args: any[]) => Promise<any>;
|
||||
};
|
||||
[member: string]: any;
|
||||
}
|
||||
@@ -201,7 +201,7 @@ describeEmbeddedPostgres("access routes permissions upgrade compatibility", () =
|
||||
companyId: company.id,
|
||||
applicationKey: `route-app-${randomUUID()}`,
|
||||
name: "Route personal app",
|
||||
type: "mcp",
|
||||
type: "mcp_http",
|
||||
status: "active",
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
const connection = await db.insert(toolConnections).values({
|
||||
|
||||
@@ -272,7 +272,7 @@ describeEmbeddedPostgres("access service", () => {
|
||||
companyId: company.id,
|
||||
applicationKey: `instance-access-${randomUUID()}`,
|
||||
name: "Instance access app",
|
||||
type: "mcp",
|
||||
type: "mcp_http",
|
||||
status: "active",
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
const connection = await db.insert(toolConnections).values({
|
||||
@@ -333,7 +333,7 @@ describeEmbeddedPostgres("access service", () => {
|
||||
companyId: company.id,
|
||||
applicationKey: `app-${randomUUID()}`,
|
||||
name: "Personal mail",
|
||||
type: "mcp",
|
||||
type: "mcp_http",
|
||||
status: "active",
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
const connection = await db.insert(toolConnections).values({
|
||||
@@ -428,7 +428,7 @@ describeEmbeddedPostgres("access service", () => {
|
||||
companyId: company.id,
|
||||
applicationKey: `shared-personal-${randomUUID()}`,
|
||||
name: "Shared personal app",
|
||||
type: "mcp",
|
||||
type: "mcp_http",
|
||||
status: "active",
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
const connection = await db.insert(toolConnections).values({
|
||||
@@ -505,7 +505,7 @@ describeEmbeddedPostgres("access service", () => {
|
||||
companyId: company.id,
|
||||
applicationKey: `mixed-${randomUUID()}`,
|
||||
name: "Mixed credential app",
|
||||
type: "mcp",
|
||||
type: "mcp_http",
|
||||
status: "active",
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
const definition = await db.insert(userSecretDefinitions).values({
|
||||
@@ -595,7 +595,7 @@ describeEmbeddedPostgres("access service", () => {
|
||||
companyId: company.id,
|
||||
applicationKey: `shared-org-${randomUUID()}`,
|
||||
name: "Shared organization app",
|
||||
type: "mcp",
|
||||
type: "mcp_http",
|
||||
status: "active",
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
const connection = await db.insert(toolConnections).values({
|
||||
@@ -692,7 +692,7 @@ describeEmbeddedPostgres("access service", () => {
|
||||
companyId: company.id,
|
||||
applicationKey: `app-${randomUUID()}`,
|
||||
name: "Personal mail",
|
||||
type: "mcp",
|
||||
type: "mcp_http",
|
||||
status: "active",
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
const connection = await db.insert(toolConnections).values({
|
||||
@@ -754,7 +754,7 @@ describeEmbeddedPostgres("access service", () => {
|
||||
companyId: company.id,
|
||||
applicationKey: `app-${randomUUID()}`,
|
||||
name: "Personal shared credentials",
|
||||
type: "mcp",
|
||||
type: "mcp_http",
|
||||
status: "active",
|
||||
}).returning().then((rows) => rows[0]!);
|
||||
const definitions = await db.insert(userSecretDefinitions).values([
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("resolveResponsibleUserIdForActivity", () => {
|
||||
});
|
||||
|
||||
it("prefers the heartbeat run responsible user", async () => {
|
||||
const db = createReader(new Map([
|
||||
const db = createReader(new Map<unknown, Array<Record<string, unknown>>>([
|
||||
[heartbeatRuns, [{ responsibleUserId: "run-user" }]],
|
||||
[issues, [{ responsibleUserId: "issue-user", createdByUserId: null }]],
|
||||
[agentApiKeys, [{ responsibleUserId: "key-user" }]],
|
||||
@@ -89,7 +89,7 @@ describe("resolveResponsibleUserIdForActivity", () => {
|
||||
});
|
||||
|
||||
it("falls back to issue attribution when the run is unavailable", async () => {
|
||||
const db = createReader(new Map([
|
||||
const db = createReader(new Map<unknown, Array<Record<string, unknown>>>([
|
||||
[heartbeatRuns, []],
|
||||
[issues, [{ responsibleUserId: "issue-user", createdByUserId: "creator-user" }]],
|
||||
[agentApiKeys, [{ responsibleUserId: "key-user" }]],
|
||||
@@ -103,7 +103,7 @@ describe("resolveResponsibleUserIdForActivity", () => {
|
||||
});
|
||||
|
||||
it("uses explicit issue context for non-issue activity", async () => {
|
||||
const db = createReader(new Map([
|
||||
const db = createReader(new Map<unknown, Array<Record<string, unknown>>>([
|
||||
[issues, [{ responsibleUserId: "issue-user", createdByUserId: null }]],
|
||||
[companies, [{ defaultResponsibleUserId: "default-user" }]],
|
||||
]));
|
||||
@@ -116,7 +116,7 @@ describe("resolveResponsibleUserIdForActivity", () => {
|
||||
});
|
||||
|
||||
it("uses the active agent API key responsible user for no-run actions", async () => {
|
||||
const db = createReader(new Map([
|
||||
const db = createReader(new Map<unknown, Array<Record<string, unknown>>>([
|
||||
[agentApiKeys, [{ responsibleUserId: "key-user" }]],
|
||||
[companies, [{ defaultResponsibleUserId: "default-user" }]],
|
||||
]));
|
||||
@@ -129,7 +129,7 @@ describe("resolveResponsibleUserIdForActivity", () => {
|
||||
});
|
||||
|
||||
it("falls back to the company default responsible user", async () => {
|
||||
const db = createReader(new Map([
|
||||
const db = createReader(new Map<unknown, Array<Record<string, unknown>>>([
|
||||
[agentApiKeys, []],
|
||||
[companies, [{ defaultResponsibleUserId: "default-user" }]],
|
||||
]));
|
||||
@@ -142,7 +142,7 @@ describe("resolveResponsibleUserIdForActivity", () => {
|
||||
});
|
||||
|
||||
it("uses issue creator attribution when responsibleUserId is absent", async () => {
|
||||
const db = createReader(new Map([
|
||||
const db = createReader(new Map<unknown, Array<Record<string, unknown>>>([
|
||||
[issues, [{ responsibleUserId: null, createdByUserId: "creator-user" }]],
|
||||
[companies, [{ defaultResponsibleUserId: "default-user" }]],
|
||||
]));
|
||||
@@ -151,7 +151,7 @@ describe("resolveResponsibleUserIdForActivity", () => {
|
||||
});
|
||||
|
||||
it("ignores malformed UUID-backed identifiers", async () => {
|
||||
const db = createReader(new Map([
|
||||
const db = createReader(new Map<unknown, Array<Record<string, unknown>>>([
|
||||
[heartbeatRuns, [{ responsibleUserId: "run-user" }]],
|
||||
[issues, [{ responsibleUserId: "issue-user", createdByUserId: null }]],
|
||||
[agentApiKeys, [{ responsibleUserId: "key-user" }]],
|
||||
|
||||
@@ -4,6 +4,7 @@ import request from "supertest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { vi } from "vitest";
|
||||
import type { ServerAdapterModule } from "../adapters/index.js";
|
||||
import type { RunnerTarget } from "../services/agent-runner-selection.js";
|
||||
|
||||
const mockAdapterPluginStore = vi.hoisted(() => ({
|
||||
listAdapterPlugins: vi.fn(),
|
||||
@@ -221,7 +222,7 @@ describe("adapter routes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
it.each<[RunnerTarget, string]>([
|
||||
[{ driver: "ssh", platform: "linux", architecture: "x64" }, "paperclip"],
|
||||
[{ driver: "ssh", platform: "darwin", architecture: "arm64" }, "paperclip"],
|
||||
[{ driver: "ssh", platform: "darwin", architecture: "x64" }, "paperclip"],
|
||||
|
||||
@@ -177,12 +177,12 @@ describePostgres("agent action audit routes", () => {
|
||||
it("denies agents and board users without the audit permission", async () => {
|
||||
const { company, agent } = await seed();
|
||||
const agentResponse = await request(await createApp(db, {
|
||||
type: "agent", agentId: agent.id, companyId: company.id, runId: null, source: "agent_jwt",
|
||||
type: "agent", agentId: agent.id, companyId: company.id, source: "agent_jwt",
|
||||
})).get(`/api/companies/${company.id}/audit/agent-actions`);
|
||||
expect(agentResponse.status).toBe(403);
|
||||
|
||||
const agentAllActorsResponse = await request(await createApp(db, {
|
||||
type: "agent", agentId: agent.id, companyId: company.id, runId: null, source: "agent_jwt",
|
||||
type: "agent", agentId: agent.id, companyId: company.id, source: "agent_jwt",
|
||||
})).get(`/api/companies/${company.id}/audit/agent-actions?actorScope=all`);
|
||||
expect(agentAllActorsResponse.status, JSON.stringify(agentAllActorsResponse.body)).toBe(200);
|
||||
expect(agentAllActorsResponse.body.items).toHaveLength(3);
|
||||
|
||||
@@ -307,7 +307,7 @@ describe("agent auth middleware", () => {
|
||||
it.each(["key", "jwt"])("rejects unfinished setup authentication with a %s", async kind => {
|
||||
const companyId = randomUUID();
|
||||
const agentId = randomUUID();
|
||||
const token = kind === "jwt" ? createLocalAgentJwt(agentId, companyId, "codex_local", randomUUID(), "user-1") : "pcp_setup-test-token";
|
||||
const token = kind === "jwt" ? createLocalAgentJwt(agentId, companyId, "codex_local", randomUUID(), "user-1")! : "pcp_setup-test-token";
|
||||
const { db } = createDbState({
|
||||
agent: { id: agentId, companyId, status: "paused", lifecycleHolds: ["setup"] },
|
||||
...(kind === "key" ? { agentKey: { id: randomUUID(), agentId, companyId, keyHash: hashToken(token), responsibleUserId: "user-1" } } : {}),
|
||||
|
||||
@@ -147,7 +147,7 @@ import { recoveryPostgresSupport as support } from "./helpers/recovery-postgres.
|
||||
const locked = new Promise<number>(resolve => { ready = resolve; });
|
||||
const release = new Promise<void>(resolve => { unlock = resolve; });
|
||||
const gate = server.db.transaction(async tx => {
|
||||
const [{ pid }] = await tx.execute(sql<{ pid: number }>`select pg_backend_pid() as pid`);
|
||||
const [{ pid }] = await tx.execute<{ pid: number }>(sql`select pg_backend_pid() as pid`);
|
||||
await tx.select({ id: issues.id }).from(issues).where(eq(issues.id, receipt.conversationId)).for("update");
|
||||
ready(pid); await release;
|
||||
});
|
||||
@@ -263,7 +263,7 @@ import { recoveryPostgresSupport as support } from "./helpers/recovery-postgres.
|
||||
const locked = new Promise<number>(resolve => { ready = resolve; });
|
||||
const release = new Promise<void>(resolve => { unlock = resolve; });
|
||||
const gate = server.db.transaction(async tx => {
|
||||
const [{ pid }] = await tx.execute(sql<{ pid: number }>`select pg_backend_pid() as pid`);
|
||||
const [{ pid }] = await tx.execute<{ pid: number }>(sql`select pg_backend_pid() as pid`);
|
||||
await tx.select({ id: agents.id }).from(agents).where(eq(agents.id, f.agentId)).for("update");
|
||||
ready(pid);
|
||||
await release;
|
||||
|
||||
@@ -71,7 +71,7 @@ describe("agent commentary browsing", () => {
|
||||
await db.insert(agents).values({ id: agentId, companyId: pagedCompanyId, name: "Writer", role: "engineer", adapterType: "codex_local" });
|
||||
await db.insert(heartbeatRuns).values({ id: runId, companyId: pagedCompanyId, agentId, status: "succeeded" });
|
||||
await db.insert(agentCommentary).values(Array.from({ length: 58 }, (_, index) => ({
|
||||
companyId: pagedCompanyId, agentId, runId, kind: "suggestion", body: `Message ${index}`,
|
||||
companyId: pagedCompanyId, agentId, runId, kind: "suggestion" as const, body: `Message ${index}`,
|
||||
idempotencyKey: String(index), payloadHash: "hash",
|
||||
})));
|
||||
const first = await reader.list(pagedCompanyId, query(), sql`true`);
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
undeliveredConversationComments,
|
||||
} from "../services/agent-conversations.js";
|
||||
import { classifyIssueGraphLiveness } from "../services/recovery/issue-graph-liveness.js";
|
||||
import { NATIVE_STATUS_ARBITER_POLICY_VERSION } from "../services/native-runtime/status-arbiter.js";
|
||||
import { runningProcesses } from "../adapters/index.js";
|
||||
import {
|
||||
buildPaperclipTaskMarkdown,
|
||||
@@ -444,9 +445,9 @@ import {
|
||||
});
|
||||
const [interaction] = await db.insert(issueThreadInteractions).values({
|
||||
companyId, issueId: issue.id, kind: "request_confirmation", status: "rejected",
|
||||
payload: { version: 1, target: { type: "issue_document", key: "plan", documentId: document.id,
|
||||
payload: { version: 1, prompt: "Approve the plan?", target: { type: "issue_document", key: "plan", documentId: document.id,
|
||||
revisionId: document.latestRevisionId!, revisionNumber: document.latestRevisionNumber } },
|
||||
result: { outcome: "rejected", reason: "Include CHAT_REVIEW_MARKER in the revised plan." },
|
||||
result: { version: 1, outcome: "rejected", reason: "Include CHAT_REVIEW_MARKER in the revised plan." },
|
||||
}).returning();
|
||||
const input = { db, companyId, issueSummary: { ...issue, workMode: "planning" },
|
||||
contextSnapshot: { issueId: issue.id, conversationMode: true, interactionId: interaction!.id,
|
||||
@@ -467,7 +468,7 @@ import {
|
||||
contextSnapshot: { issueId: issue.id, conversationMode: true } });
|
||||
expect(later?.planReviewContext).toBeNull();
|
||||
// An unrelated confirmation must not cause old plan context to be replayed.
|
||||
await db.update(issueThreadInteractions).set({ payload: { version: 1 } })
|
||||
await db.update(issueThreadInteractions).set({ payload: { version: 1, prompt: "Proceed?" } })
|
||||
.where(eq(issueThreadInteractions.id, interaction!.id));
|
||||
expect((await buildPaperclipWakePayload(input))?.planReviewContext).toBeNull();
|
||||
});
|
||||
@@ -769,7 +770,7 @@ import {
|
||||
const succeeded = { ...run, contextSnapshot: prepared.context, status: "succeeded" };
|
||||
const [interaction] = await db.insert(issueThreadInteractions).values({
|
||||
companyId, issueId: chat.id, sourceRunId: run.id, createdByAgentId: agentId,
|
||||
kind: "request_confirmation", status: "answered", payload: { version: 1 },
|
||||
kind: "request_confirmation", status: "answered", payload: { version: 1, prompt: "Proceed?" },
|
||||
}).returning();
|
||||
expect(await settleConversationTurn(db, succeeded)).toBe(false);
|
||||
await db.update(issueThreadInteractions).set({ status: "pending", sourceRunId: null })
|
||||
@@ -831,8 +832,8 @@ import {
|
||||
blockedByIssueIds: [blocker.id],
|
||||
});
|
||||
for (const state of [
|
||||
{ status: "in_review", conversationState: "waiting" },
|
||||
{ status: "blocked", conversationState: "active" },
|
||||
{ status: "in_review", conversationState: "waiting" as const },
|
||||
{ status: "blocked", conversationState: "active" as const },
|
||||
]) {
|
||||
await db.update(issues).set(state).where(eq(issues.id, chat.id));
|
||||
for (const reason of ["issue_blockers_resolved", "issue_children_completed", "issue_unblock_requested"]) {
|
||||
@@ -914,19 +915,21 @@ import {
|
||||
const answerRun = await runFor(chat.id, first.id, { source: "issue.interaction.respond", interactionId: question.id,
|
||||
interactionKind: "ask_user_questions", interactionStatus: "answered" });
|
||||
const prepared = await prepareConversationTurn(db, answerRun);
|
||||
expect(prepared.context.wakeCommentId).toBe(first.id);
|
||||
expect(prepared.context.conversationReplyBoundaryCommentId).toBe(["answered", "new-message"].includes(scenario) ? newer.id : first.id);
|
||||
const preparedContext: Record<string, unknown> = prepared.context;
|
||||
expect(preparedContext.wakeCommentId).toBe(first.id);
|
||||
expect(preparedContext.conversationReplyBoundaryCommentId).toBe(["answered", "new-message"].includes(scenario) ? newer.id : first.id);
|
||||
// A retry reuses the frozen boundary, even if another message has arrived.
|
||||
if (scenario === "new-message") await svc.addComment(chat.id, "New instructions while you answer", { userId: "local-board" });
|
||||
const retry = await prepareConversationTurn(db, { ...answerRun, contextSnapshot: prepared.context });
|
||||
expect(retry.context.conversationReplyBoundaryCommentId).toBe(prepared.context.conversationReplyBoundaryCommentId);
|
||||
const retryContext: Record<string, unknown> = retry.context;
|
||||
expect(retryContext.conversationReplyBoundaryCommentId).toBe(preparedContext.conversationReplyBoundaryCommentId);
|
||||
await svc.addComment(chat.id, "You chose Blue.", { agentId, runId: answerRun.id });
|
||||
expect(await settleConversationTurn(db, { ...answerRun, status: "succeeded", contextSnapshot: prepared.context })).toBe(true);
|
||||
const [settled] = await db.select().from(issues).where(eq(issues.id, chat.id));
|
||||
expect(settled.conversationState).toBe(scenario === "answered" ? "waiting" : "active");
|
||||
expect(retry.context.conversationReplayThroughCommentId).toBe(prepared.context.conversationReplayThroughCommentId);
|
||||
expect(retryContext.conversationReplayThroughCommentId).toBe(preparedContext.conversationReplayThroughCommentId);
|
||||
const replay = await conversationReplay(db, companyId, chat.id, first.id,
|
||||
prepared.context.conversationReplayThroughCommentId as string);
|
||||
preparedContext.conversationReplayThroughCommentId as string);
|
||||
if (["answered", "new-message"].includes(scenario)) {
|
||||
expect(replay).toContain("What is the capital of France?");
|
||||
expect(replay).toContain("Paris.");
|
||||
@@ -1026,7 +1029,7 @@ describe("chat prompt policy", () => {
|
||||
|
||||
it.each([true, false])("preserves rejected-plan changes in task markdown (includeDescription=%s)", (includeDescription) => {
|
||||
const prompt = buildPaperclipTaskMarkdown({
|
||||
issue: { id: "chat", title: "Chat", workMode: "planning", conversationAgentId: "agent" },
|
||||
issue: { id: "chat", identifier: null, title: "Chat", workMode: "planning", conversationAgentId: "agent" },
|
||||
interaction: { kind: "request_confirmation", status: "rejected" },
|
||||
planReview: { status: "rejected", reason: "Add CHAT_REVIEW_MARKER and a validation step." },
|
||||
acceptedPlanContinuation: true,
|
||||
@@ -1070,7 +1073,7 @@ describe("chat prompt policy", () => {
|
||||
describe("native conversation finalization", () => {
|
||||
it("does not require execution completion or schedule a continuation after a successful chat turn", () => {
|
||||
const decision = {
|
||||
policyVersion: "paperclip.native-status-arbiter.v1",
|
||||
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
|
||||
statusAction: "in_progress",
|
||||
toStatus: "in_progress",
|
||||
reasonCode: "completion_evidence_incomplete",
|
||||
|
||||
@@ -47,6 +47,7 @@ let currentKeyAgentId = agentId;
|
||||
let currentAccessCanUser = false;
|
||||
|
||||
const mockAgentService = vi.hoisted(() => ({
|
||||
create: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
retryLifecycle: vi.fn(),
|
||||
pause: vi.fn(),
|
||||
|
||||
@@ -12,7 +12,7 @@ import { agentJoinGrantsFromDefaults } from "../services/invite-grants.js";
|
||||
|
||||
const support = await getEmbeddedPostgresTestSupport();
|
||||
const describeDatabase = support.supported ? describe : describe.skip;
|
||||
const expectedNewAgentGrantKeys: PermissionKey[] = [
|
||||
const expectedNewAgentGrantKeys: PermissionKey[] = ([
|
||||
"agents:read",
|
||||
"agents:configure",
|
||||
"agents:suggest-changes",
|
||||
@@ -28,7 +28,7 @@ const expectedNewAgentGrantKeys: PermissionKey[] = [
|
||||
"tasks:assign",
|
||||
"tasks:assign_scope",
|
||||
"tasks:manage_active_checkouts",
|
||||
].sort();
|
||||
] satisfies PermissionKey[]).sort();
|
||||
|
||||
describeDatabase("new agent configuration defaults", () => {
|
||||
let db!: ReturnType<typeof createDb>;
|
||||
|
||||
@@ -51,6 +51,7 @@ const GROK_PROMPT_OUTPUT = [
|
||||
const CREDENTIAL_BYTES = '{"tokens":{"access":"SECRET-ACCESS-TOKEN"}}';
|
||||
|
||||
const mockAgentService = vi.hoisted(() => ({
|
||||
create: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
getChainOfCommand: vi.fn(async () => []),
|
||||
}));
|
||||
|
||||
@@ -182,7 +182,7 @@ describe("stable live agent directory observation", () => {
|
||||
traversal: `${reserved}/../../scratch`, "trailing-slash": `${reserved}/scratch/`, nul: `${reserved}/scratch\0`,
|
||||
}[kind]!;
|
||||
await expect(prepareSandboxManagedRuntime({
|
||||
spec: { transport: "sandbox", provider: "fixture", sandboxId: "owned", remoteCwd: root, apiKey: null },
|
||||
spec: { transport: "sandbox", provider: "fixture", sandboxId: "owned", remoteCwd: root, timeoutMs: 30_000, apiKey: null },
|
||||
adapterKey: "agent-files", client: {} as never, workspaceLocalDir: root, runtimeRootDir: invalid,
|
||||
})).rejects.toThrow("Transfer scratch must remain within the lease runtime tree");
|
||||
expect(fs.existsSync(reserved)).toBe(false);
|
||||
|
||||
@@ -908,10 +908,10 @@ describe("persistent agent directories", () => {
|
||||
it("cleans up a staging failure before any provider starts", async () => {
|
||||
const runId = randomUUID();
|
||||
await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, invocationSource: "on_demand", responsibleUserId: userId });
|
||||
const shell = vi.spyOn(executionTargetTools, "runAdapterExecutionTargetShellCommand").mockResolvedValue({ exitCode: 1, signal: null, timedOut: false, stdout: "", stderr: "fixture failure" });
|
||||
const shell = vi.spyOn(executionTargetTools, "runAdapterExecutionTargetShellCommand").mockResolvedValue({ exitCode: 1, signal: null, timedOut: false, stdout: "", stderr: "fixture failure", pid: null, startedAt: null });
|
||||
try {
|
||||
await expect(copies.prepare({ ...target(), runId, cwd: home, target: { kind: "remote", transport: "ssh", environmentId: randomUUID(), remoteCwd: "/fixture/task",
|
||||
spec: { host: "unused.invalid", port: 22, username: "test", remoteCwd: "/fixture/task" } } })).rejects.toThrow("Could not exclude");
|
||||
spec: { host: "unused.invalid", port: 22, username: "test", remoteCwd: "/fixture/task", remoteWorkspacePath: "/fixture/task", privateKey: null, knownHosts: null, strictHostKeyChecking: true } } })).rejects.toThrow("Could not exclude");
|
||||
const row = (await copies.get(companyId, runId))!;
|
||||
expect(row.state).toBe("unavailable");
|
||||
expect(row.receipt?.baseline).toBeUndefined();
|
||||
@@ -1424,7 +1424,7 @@ describe("persistent agent directories", () => {
|
||||
|
||||
it("stages SSH agent files at the registered root without a nested task workspace", async () => {
|
||||
const remoteCwd = path.join(home, "ssh-task");
|
||||
const exclude = vi.spyOn(executionTargetTools, "runAdapterExecutionTargetShellCommand").mockResolvedValue({ exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" });
|
||||
const exclude = vi.spyOn(executionTargetTools, "runAdapterExecutionTargetShellCommand").mockResolvedValue({ exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "", pid: null, startedAt: null });
|
||||
const stage = vi.spyOn(ssh, "syncDirectoryToSsh").mockImplementation(async input => {
|
||||
await fs.mkdir(path.dirname(input.remoteDir), { recursive: true });
|
||||
await fs.cp(input.localDir, input.remoteDir, { recursive: true });
|
||||
@@ -1436,7 +1436,7 @@ describe("persistent agent directories", () => {
|
||||
const runId = randomUUID();
|
||||
await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, invocationSource: "on_demand", responsibleUserId: userId });
|
||||
const target = { kind: "remote" as const, transport: "ssh" as const, environmentId: randomUUID(), remoteCwd,
|
||||
spec: { host: "unused.invalid", port: 22, username: "test", remoteCwd } };
|
||||
spec: { host: "unused.invalid", port: 22, username: "test", remoteCwd, remoteWorkspacePath: remoteCwd, privateKey: null, knownHosts: null, strictHostKeyChecking: true } };
|
||||
await db.insert(environments).values({ id: target.environmentId, name: "Fixture SSH", driver: "ssh" });
|
||||
// Legacy no-ID receipt: only the unique run/environment lease authorizes retrieval.
|
||||
await db.insert(environmentLeases).values({ companyId, environmentId: target.environmentId, heartbeatRunId: runId, provider: "ssh", providerLeaseId: "fixture-host" });
|
||||
@@ -1606,8 +1606,8 @@ describe("persistent agent directories", () => {
|
||||
const { copy, lease } = await unavailableRemote({ state: "destroyed" });
|
||||
const remoteCwd = "/fixture/cleanup-task";
|
||||
const executionTarget = { kind: "remote" as const, transport: "ssh" as const, environmentId: lease.environmentId, leaseId: lease.id, remoteCwd,
|
||||
spec: { host: "unused.invalid", port: 22, username: "test", remoteCwd } };
|
||||
const shell = vi.spyOn(executionTargetTools, "runAdapterExecutionTargetShellCommand").mockResolvedValue({ exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" });
|
||||
spec: { host: "unused.invalid", port: 22, username: "test", remoteCwd, remoteWorkspacePath: remoteCwd, privateKey: null, knownHosts: null, strictHostKeyChecking: true } };
|
||||
const shell = vi.spyOn(executionTargetTools, "runAdapterExecutionTargetShellCommand").mockResolvedValue({ exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "", pid: null, startedAt: null });
|
||||
const stage = vi.spyOn(ssh, "syncDirectoryToSsh").mockResolvedValue(undefined);
|
||||
let stopCommitted!: () => void, finishCleanup!: () => void;
|
||||
const committed = new Promise<void>(resolve => { stopCommitted = resolve; });
|
||||
@@ -1728,7 +1728,7 @@ describe("persistent agent directories", () => {
|
||||
await db.insert(environmentLeases).values(lease);
|
||||
const execute = vi.fn(), restoreWorkspace = vi.fn(), cleanupWorkspaceSnapshot = vi.fn().mockResolvedValue(undefined);
|
||||
const executionTarget = { kind: "remote" as const, transport: "sandbox" as const, environmentId, leaseId, remoteCwd, runner: { execute } };
|
||||
const shell = vi.spyOn(executionTargetTools, "runAdapterExecutionTargetShellCommand").mockResolvedValue({ exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" });
|
||||
const shell = vi.spyOn(executionTargetTools, "runAdapterExecutionTargetShellCommand").mockResolvedValue({ exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "", pid: null, startedAt: null });
|
||||
const stage = vi.spyOn(executionTargetTools, "prepareAdapterExecutionTargetRuntime").mockImplementation(async options => ({ target: executionTarget,
|
||||
workspaceRemoteDir: path.posix.join(remoteCwd, ".paperclip-runtime", "agent-files", agentId, runId), runtimeRootDir: options.runtimeRootDir ?? null,
|
||||
assetDirs: {}, additionalSourceDirs: {}, additionalSourceFailures: [], workspaceSyncSnapshot: null, restoreWorkspace, cleanupWorkspaceSnapshot }));
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("incremental managed file checkpoints", () => {
|
||||
if (first) { first = false; await fs.appendFile(filename, Buffer.alloc(1024 * 1024, 2)); }
|
||||
yield chunk;
|
||||
}
|
||||
})()) as typeof handle.createReadStream;
|
||||
})()) as unknown as typeof handle.createReadStream;
|
||||
}
|
||||
return handle;
|
||||
});
|
||||
|
||||
@@ -297,7 +297,7 @@ describe("agent-created hires use managed AI connections", () => {
|
||||
const otherOwner = `other-${f.companyId}`;
|
||||
await db.insert(companyMemberships).values({ companyId: f.companyId, principalType: "user", principalId: otherOwner, membershipRole: "member", status: "active" });
|
||||
const account = await aiConnectionService(db).save(f.companyId, otherOwner, { provider: "openai", method: "api_key", ownership: "shared", name: "Shared credential, restricted installation", apiKey: "fixture", agentIds: [f.agentId], allAgents: false }, "fixture");
|
||||
await toolAccessService(db).replaceConnectionGrantMembers(account.connectionId, account.grantId, [f.userId], { userId: otherOwner });
|
||||
await toolAccessService(db).replaceConnectionGrantMembers(account.connectionId, account.grantId, [f.userId], { actorType: "user", actorId: otherOwner });
|
||||
const member: AiConnectionPoolMember = { id: randomUUID(), binding: { ...account, provider: "openai", method: "api_key", mode: "shared" }, profile: { provider: "codex", model: "gpt-5.6-sol" } };
|
||||
const p = await poolFixture(f, [member]);
|
||||
const agent = hired(await request(f.app).post(`/api/companies/${f.companyId}/agents`).send({ name: "Restricted pool teammate", role: "engineer", adapterType: f.adapterType, runtimeConfig: { aiConnection: p.binding } }));
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("agent cryptographic identity", () => {
|
||||
await execute({
|
||||
runId: randomUUID(), agent, agentIdentity: identity,
|
||||
config: { command: process.execPath, cwd: directory, args: ["-e", `const {sign}=require('node:crypto'); process.stdout.write(sign(null,Buffer.from(process.env.CHALLENGE,'base64'),process.env.PAPERCLIP_AGENT_PRIVATE_KEY).toString('base64'));`], env: { CHALLENGE: challenge, PAPERCLIP_AGENT_PRIVATE_KEY: "configured override" } },
|
||||
context: {}, runtime: { sessionId: null, sessionParams: null, taskKey: null },
|
||||
context: {}, runtime: { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null },
|
||||
onLog: async (_stream, chunk) => { logs.push(chunk); }, onMeta: async meta => { metas.push(meta); },
|
||||
});
|
||||
expect(verify(null, Buffer.from(challenge, "base64"), publicPem, Buffer.from(logs.join(""), "base64"))).toBe(true);
|
||||
|
||||
@@ -43,7 +43,7 @@ describe.skipIf(!support.supported)("agent-scoped runner qualification", () => {
|
||||
await db.insert(authUsers).values({ id: userId, name: "Company owner", email: `${userId}@example.test`, emailVerified: true, createdAt: new Date(), updatedAt: new Date() });
|
||||
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: userId, membershipRole: "owner", status: "active" });
|
||||
await db.insert(principalPermissionGrants).values(grantsForHumanRole("owner").map(grant => ({ ...grant, companyId, principalType: "user" as const, principalId: userId })));
|
||||
await db.insert(environments).values({ id: environmentId, name: `Paperclip Computer ${environmentId}`, driver: "sandbox", status: "active", config: { provider: "daytona", snapshot: "managed-snapshot" } });
|
||||
await db.insert(environments).values({ id: environmentId, name: `Paperclip Computer ${environmentId}`, driver: "sandbox", status: "active", config: { provider: "daytona", snapshot: "managed-snapshot", reuseLease: false } });
|
||||
await db.insert(agents).values({ id: agentId, companyId, name: "Amy fixture", adapterType: "claude_local", defaultEnvironmentId: environmentId,
|
||||
adapterConfig: { model: "claude-opus-5-5", effort: "high", maxTurnsPerRun: 1000, dangerouslySkipPermissions: true } });
|
||||
app = express(); app.use(express.json());
|
||||
@@ -89,7 +89,7 @@ describe.skipIf(!support.supported)("agent-scoped runner qualification", () => {
|
||||
|
||||
it("reuses a current native snapshot proof without invalidating it or allocating another probe", async () => {
|
||||
await db.update(agents).set({ adapterType: "paperclip_runner", adapterConfig: { provider: "opencode" } }).where(eq(agents.id, agentId));
|
||||
const config = { provider: "daytona", snapshot: "managed-snapshot" };
|
||||
const config = { provider: "daytona", snapshot: "managed-snapshot", reuseLease: false };
|
||||
const fingerprint = fingerprintEnvironmentSandboxProviderConfig(config);
|
||||
const saved = (await environmentService(db).getById(environmentId))!;
|
||||
const runtime = await resolveEnvironmentDriverConfigForRuntime(db, companyId, saved, { applyCustomImageTemplate: true, readOnlyCustomImageTemplate: true });
|
||||
|
||||
@@ -98,14 +98,14 @@ describeDatabase("connection assignment synchronization", () => {
|
||||
const [project] = await db.insert(projects).values({ companyId, name: "Project binding fixture" }).returning();
|
||||
for (let round = 0; round < 2; round++) {
|
||||
let before = (await events()).length;
|
||||
await service.bindProfile(profile.id, { targetType: "agent", targetId: agentId });
|
||||
await service.bindProfile(profile.id, { targetType: "agent", targetId: agentId, priority: 100 });
|
||||
expect((await events()).slice(before).map(event => event.resourceId)).toEqual([agentId]);
|
||||
before = (await events()).length;
|
||||
await service.unbindProfile(profile.id, { targetType: "agent", targetId: agentId });
|
||||
expect((await events()).slice(before).map(event => event.resourceId)).toEqual([agentId]);
|
||||
}
|
||||
const beforeProject = await events();
|
||||
await service.bindProfile(profile.id, { targetType: "project", targetId: project.id });
|
||||
await service.bindProfile(profile.id, { targetType: "project", targetId: project.id, priority: 100 });
|
||||
await service.unbindProfile(profile.id, { targetType: "project", targetId: project.id });
|
||||
expect(await events()).toEqual(beforeProject);
|
||||
let before = (await events()).length;
|
||||
@@ -121,7 +121,7 @@ describeDatabase("connection assignment synchronization", () => {
|
||||
await db.update(toolConnectionInstalls).set({ targetId: peerId }).where(eq(toolConnectionInstalls.id, install.id));
|
||||
expect((await events()).slice(before).map(event => event.resourceId).sort()).toEqual([agentId, peerId].sort());
|
||||
before = (await events()).length;
|
||||
await service.bindProfile(profile.id, { targetType: "company", targetId: companyId });
|
||||
await service.bindProfile(profile.id, { targetType: "company", targetId: companyId, priority: 100 });
|
||||
expect((await events()).slice(before).map(event => event.resourceId).sort()).toEqual([agentId, peerId].sort());
|
||||
});
|
||||
it("keeps connector changes and their events atomic and company-scoped", async () => {
|
||||
@@ -217,9 +217,9 @@ describeDatabase("connection assignment synchronization", () => {
|
||||
const single = createDb(database.connectionString, { maxConnections: 1 });
|
||||
const service = toolAccessService(single);
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const work = change === "bind" ? service.bindProfile(source.id, { targetType: "agent", targetId: agentId })
|
||||
const work = change === "bind" ? service.bindProfile(source.id, { targetType: "agent", targetId: agentId, priority: 100 })
|
||||
: change === "unbind" ? service.unbindProfile(source.id, { targetType: "agent", targetId: agentId })
|
||||
: service.deleteProfile(source.id, change === "reassign" ? { reassignToProfileId: target.id } : { force: true });
|
||||
: service.deleteProfile(source.id, change === "reassign" ? { force: false, reassignToProfileId: target.id } : { force: true });
|
||||
const settled = Promise.allSettled([work]);
|
||||
try {
|
||||
await Promise.race([work, new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error("Profile operation waited for another pooled connection")), 3000); })]);
|
||||
@@ -261,9 +261,9 @@ describeDatabase("connection assignment synchronization", () => {
|
||||
expect(activity.wait_event_type).toBe("Lock");
|
||||
}, { timeout: 5000 });
|
||||
const mutation = run(writer => change === "bind"
|
||||
? writer.bindProfile(profile.id, { targetType: "agent", targetId: targetAgent })
|
||||
? writer.bindProfile(profile.id, { targetType: "agent", targetId: targetAgent, priority: 100 })
|
||||
: change === "unbind" ? writer.unbindProfile(profile.id, { targetType: "agent", targetId: targetAgent })
|
||||
: writer.deleteProfile(source!.id, { reassignToProfileId: profile.id }));
|
||||
: writer.deleteProfile(source!.id, { force: false, reassignToProfileId: profile.id }));
|
||||
const mutationResult = Promise.allSettled([mutation]);
|
||||
let finishResult: Promise<PromiseSettledResult<unknown>[]> | undefined;
|
||||
try {
|
||||
@@ -324,7 +324,7 @@ describeDatabase("connection assignment synchronization", () => {
|
||||
const firstResult = Promise.allSettled([first]);
|
||||
try {
|
||||
await waitForLock(0);
|
||||
second = Promise.allSettled([run(change === "connection settings" ? adopt : client => toolAccessService(client).bindProfile(bindingProfile!.id, { targetType: "agent", targetId: lower }, { actorType: "agent", actorId: higher }))]);
|
||||
second = Promise.allSettled([run(change === "connection settings" ? adopt : client => toolAccessService(client).bindProfile(bindingProfile!.id, { targetType: "agent", targetId: lower, priority: 100 }, { actorType: "agent", actorId: higher }))]);
|
||||
await waitForLock(1);
|
||||
} finally { release(); await blocker; }
|
||||
for (const result of [...await firstResult, ...await second!]) if (result.status === "rejected") throw result.reason;
|
||||
@@ -373,7 +373,7 @@ describeDatabase("connection assignment synchronization", () => {
|
||||
await waitForLock(0);
|
||||
replacement = Promise.allSettled([run(service => change === "installs"
|
||||
? service.putConnectionInstalls(connection.id, { installs: [{ targetType: "agent", targetId: lower }] })
|
||||
: change === "reassign" ? service.deleteProfile(source.id, { reassignToProfileId: target.id })
|
||||
: change === "reassign" ? service.deleteProfile(source.id, { force: false, reassignToProfileId: target.id })
|
||||
: service.finishGalleryAppConnection(companyId, connection.id, { enabledCatalogEntryIds: [], askFirstCatalogEntryIds: [], access: { agentIds: [change === "finish" ? lower : higher] } }))]);
|
||||
await waitForLock(1);
|
||||
} finally { release(); await blocker; }
|
||||
|
||||
@@ -72,7 +72,7 @@ describeDatabase("connector access runtime synchronization", () => {
|
||||
let memberId: string | undefined;
|
||||
if (change === "uninstall") {
|
||||
await toolAccessService(db).putConnectionInstalls(connectionId, { installs: [{ targetType: "agent", targetId: agentId }] });
|
||||
expect((await toolAccessService(db).getConnection(connectionId, companyId)).config.mcpAgentAccessConfigured).toBe(true);
|
||||
expect((await toolAccessService(db).getConnection(connectionId, companyId)).config?.mcpAgentAccessConfigured).toBe(true);
|
||||
first = client => toolAccessService(client).putConnectionInstalls(connectionId, { installs: [] });
|
||||
second = client => toolAccessService(client).finishGalleryAppConnection(companyId, connectionId, { enabledCatalogEntryIds: [], askFirstCatalogEntryIds: [], access: "all_agents" });
|
||||
} else {
|
||||
@@ -80,7 +80,7 @@ describeDatabase("connector access runtime synchronization", () => {
|
||||
memberId = member.id;
|
||||
await db.insert(principalPermissionGrants).values({ companyId, principalType: "user", principalId: "alice", permissionKey: "agents:create" });
|
||||
first = client => accessService(client).updateMemberAndPermissions(companyId, member.id, { membershipRole: "admin", grants: [{ permissionKey: "agents:create" }] }, null);
|
||||
second = client => accessService(client).setMemberPermissions(companyId, member.id, [{ permissionKey: "issues:create" }], null);
|
||||
second = client => accessService(client).setMemberPermissions(companyId, member.id, [{ permissionKey: "tasks:assign" }], null);
|
||||
}
|
||||
let locked!: () => void; let release!: () => void;
|
||||
const held = new Promise<void>(resolve => { locked = resolve; });
|
||||
@@ -118,7 +118,7 @@ describeDatabase("connector access runtime synchronization", () => {
|
||||
expect(bindings.some(binding => binding.metadata?.source === "tool_connection_install")).toBe(false);
|
||||
} else {
|
||||
expect(await accessService(db).getMemberById(companyId, memberId!)).toMatchObject({ membershipRole: "admin" });
|
||||
expect(await db.select().from(principalPermissionGrants).where(eq(principalPermissionGrants.companyId, companyId))).toEqual([expect.objectContaining({ permissionKey: "issues:create" })]);
|
||||
expect(await db.select().from(principalPermissionGrants).where(eq(principalPermissionGrants.companyId, companyId))).toEqual([expect.objectContaining({ permissionKey: "tasks:assign" })]);
|
||||
}
|
||||
}, 15000);
|
||||
it.each(["email allowAgent", "connection intent"] as const)("orders outer %s with direct install saves", async caller => {
|
||||
@@ -132,7 +132,7 @@ describeDatabase("connector access runtime synchronization", () => {
|
||||
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: userId, status: "active", membershipRole: "member" });
|
||||
await db.insert(issues).values({ id: issueId, companyId, title: "Use Notion", status: "in_progress", assigneeAgentId: agentId });
|
||||
await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, status: "running", responsibleUserId: userId, contextSnapshot: { issueId } });
|
||||
const pending = await connectionIntentService(db).request({ sub: agentId, company_id: companyId, run_id: runId, responsible_user_id: userId, scope: "connection_intents", iat: 1, exp: 2, instance_id: "test" }, "notion");
|
||||
const pending = await connectionIntentService(db).request({ sub: agentId, company_id: companyId, run_id: runId, responsible_user_id: userId }, "notion");
|
||||
expect(pending.interactionId).toEqual(expect.any(String));
|
||||
await db.update(toolConnections).set({ healthStatus: "ok", config: { sourceTemplateKey: "notion", mcpAgentAccessConfigured: true } }).where(eq(toolConnections.id, connectionId));
|
||||
await db.insert(connectionGrants).values({ companyId, connectionId, kind: "organization", status: "active", isDefault: true });
|
||||
@@ -316,7 +316,7 @@ describeDatabase("connector access runtime synchronization", () => {
|
||||
});
|
||||
it("limits delegation creation to its target without waiting on unrelated agents", async () => {
|
||||
await db.insert(companyMemberships).values(["alice", "bob"].map(principalId => ({ companyId, principalType: "user", principalId, status: "active" })));
|
||||
const grants = await db.insert(connectionGrants).values(["alice", "bob"].map(subjectUserId => ({ companyId, connectionId, kind: "user", subjectUserId }))).returning();
|
||||
const grants = await db.insert(connectionGrants).values(["alice", "bob"].map(subjectUserId => ({ companyId, connectionId, kind: "user" as const, subjectUserId }))).returning();
|
||||
const baseline = await events();
|
||||
const [lower, higher] = [agentId, peerId].sort();
|
||||
let locked!: () => void; let release!: () => void;
|
||||
|
||||
@@ -136,7 +136,7 @@ describeDatabase("tool definition runtime synchronization", () => {
|
||||
await db.update(toolConnections).set({ config: { mcpAgentAccessConfigured: true } }).where(eq(toolConnections.id, connectionId));
|
||||
await db.insert(toolProfiles).values({ companyId, profileKey: `app:${connectionId}`, name: "Install profile" });
|
||||
const [lower, higher] = [randomUUID(), randomUUID()].sort();
|
||||
await db.insert(toolPolicies).values([higher, lower].map((id, index) => ({ id, companyId, name: `Managed policy ${index}`, policyType: "require_approval", priority: (index + 1) * 100, config: { source: "app_gallery_finish", connectionId } })));
|
||||
await db.insert(toolPolicies).values([higher, lower].map((id, index) => ({ id, companyId, name: `Managed policy ${index}`, policyType: "require_approval" as const, priority: (index + 1) * 100, config: { source: "app_gallery_finish", connectionId } })));
|
||||
let locked!: () => void; let release!: () => void;
|
||||
const held = new Promise<void>(resolve => { locked = resolve; });
|
||||
const resume = new Promise<void>(resolve => { release = resolve; });
|
||||
@@ -222,10 +222,10 @@ describeDatabase("tool definition runtime synchronization", () => {
|
||||
expect(row.wait_event_type).toBe("Lock");
|
||||
}, { timeout: 5000 });
|
||||
const mutation = run(writer => change === "profile entry add"
|
||||
? writer.addProfileEntry(profile.id, { selectorType: "catalog_entry", catalogEntryId: entry.id, connectionId, applicationId })
|
||||
? writer.addProfileEntry(profile.id, { selectorType: "catalog_entry", effect: "include", catalogEntryId: entry.id, connectionId, applicationId })
|
||||
: change === "profile entry edit" ? writer.updateProfileEntry(profileEntry.id, { effect: "exclude" })
|
||||
: change === "profile entry delete" ? writer.deleteProfileEntry(profileEntry.id)
|
||||
: writer.updateProfile(profile.id, { entries: [{ selectorType: "catalog_entry", catalogEntryId: entry.id, connectionId, applicationId }] }));
|
||||
: writer.updateProfile(profile.id, { entries: [{ selectorType: "catalog_entry", effect: "include", catalogEntryId: entry.id, connectionId, applicationId }] }));
|
||||
const mutationResult = Promise.allSettled([mutation]);
|
||||
let finishResult: Promise<PromiseSettledResult<unknown>[]> | undefined;
|
||||
try {
|
||||
@@ -279,7 +279,7 @@ describeDatabase("tool definition runtime synchronization", () => {
|
||||
|
||||
it.each(["add", "edit", "delete", "replace"] as const)("completes profile entry %s with a single database connection", async change => {
|
||||
const [profile] = await db.insert(toolProfiles).values({ companyId, profileKey: "single-entry", name: "Single entry" }).returning();
|
||||
const input = { selectorType: "connection" as const, applicationId, connectionId };
|
||||
const input = { selectorType: "connection" as const, effect: "include" as const, applicationId, connectionId };
|
||||
const [entry] = await db.insert(toolProfileEntries).values({ companyId, profileId: profile.id, ...input }).returning();
|
||||
const single = createDb(database.connectionString, { maxConnections: 1 });
|
||||
const service = toolAccessService(single);
|
||||
@@ -301,7 +301,7 @@ describeDatabase("tool definition runtime synchronization", () => {
|
||||
const [profile] = await db.insert(toolProfiles).values({ companyId, profileKey: "scoped", name: "Scoped" }).returning();
|
||||
await db.insert(toolProfileBindings).values({ companyId, profileId: profile.id, targetType: "agent", targetId: agentId });
|
||||
const [entry] = await db.insert(toolProfileEntries).values({ companyId, profileId: profile.id, selectorType: "connection", connectionId }).returning();
|
||||
const [policy] = await db.insert(toolPolicies).values({ companyId, name: "Scoped", policyType: "deny", selectors: { agentId } }).returning();
|
||||
const [policy] = await db.insert(toolPolicies).values({ companyId, name: "Scoped", policyType: "block", selectors: { agentId } }).returning();
|
||||
for (let index = 0; index < 4; index++) {
|
||||
const before = (await events()).length;
|
||||
if (change === "entry") await db.update(toolProfileEntries).set({ effect: index % 2 ? "include" : "exclude" }).where(eq(toolProfileEntries.id, entry.id));
|
||||
@@ -318,7 +318,7 @@ describeDatabase("tool definition runtime synchronization", () => {
|
||||
});
|
||||
|
||||
it("uses old and new policy selectors with the execution match semantics", async () => {
|
||||
const [policy] = await db.insert(toolPolicies).values({ companyId, name: "Retarget", policyType: "deny", selectors: { agentId } }).returning();
|
||||
const [policy] = await db.insert(toolPolicies).values({ companyId, name: "Retarget", policyType: "block", selectors: { agentId } }).returning();
|
||||
const write = async (selectors: Record<string, unknown>, expected: string[]) => {
|
||||
const before = (await events()).length;
|
||||
await db.update(toolPolicies).set({ selectors }).where(eq(toolPolicies.id, policy.id));
|
||||
@@ -365,7 +365,7 @@ describeDatabase("tool definition runtime synchronization", () => {
|
||||
const [profile] = await db.insert(toolProfiles).values({ companyId, profileKey: "coalesced", name: "Coalesced" }).returning();
|
||||
await db.insert(toolProfileBindings).values({ companyId, profileId: profile.id, targetType: "agent", targetId: agentId });
|
||||
const [entry] = await db.insert(toolProfileEntries).values({ companyId, profileId: profile.id, selectorType: "connection", connectionId }).returning();
|
||||
const [policy] = await db.insert(toolPolicies).values({ companyId, name: "Coalesced", policyType: "deny", selectors: { agentId } }).returning();
|
||||
const [policy] = await db.insert(toolPolicies).values({ companyId, name: "Coalesced", policyType: "block", selectors: { agentId } }).returning();
|
||||
let before = (await events()).length;
|
||||
await db.transaction(async tx => {
|
||||
await tx.update(toolProfileEntries).set({ effect: "exclude" }).where(eq(toolProfileEntries.id, entry.id));
|
||||
@@ -383,7 +383,7 @@ describeDatabase("tool definition runtime synchronization", () => {
|
||||
|
||||
it("orders scoped policy reorder before company agent fanout", async () => {
|
||||
const [lower, higher] = [agentId, peerId].sort();
|
||||
const policies = await db.insert(toolPolicies).values([higher, lower].map((target, index) => ({ companyId, name: `Scoped ${index}`, policyType: "deny", priority: (index + 5) * 100, selectors: { agentId: target } }))).returning();
|
||||
const policies = await db.insert(toolPolicies).values([higher, lower].map((target, index) => ({ companyId, name: `Scoped ${index}`, policyType: "block" as const, priority: (index + 5) * 100, selectors: { agentId: target } }))).returning();
|
||||
let locked!: () => void; let release!: () => void;
|
||||
const held = new Promise<void>(resolve => { locked = resolve; });
|
||||
const resume = new Promise<void>(resolve => { release = resolve; });
|
||||
@@ -454,7 +454,7 @@ describeDatabase("tool definition runtime synchronization", () => {
|
||||
}, 15000);
|
||||
|
||||
it("retries changed old references before taking profile runtime locks", async () => {
|
||||
const [oldApp, newApp] = await db.insert(toolApplications).values(["Old retry app", "New retry app"].map(name => ({ companyId, name, type: "mcp_http" }))).returning();
|
||||
const [oldApp, newApp] = await db.insert(toolApplications).values(["Old retry app", "New retry app"].map(name => ({ companyId, name, type: "mcp_http" as const }))).returning();
|
||||
const [profile] = await db.insert(toolProfiles).values({ companyId, profileKey: "retry-references", name: "Retry references" }).returning();
|
||||
await db.insert(toolProfileBindings).values({ companyId, profileId: profile.id, targetType: "agent", targetId: agentId });
|
||||
const [entry] = await db.insert(toolProfileEntries).values({ companyId, profileId: profile.id, selectorType: "application", applicationId: oldApp.id }).returning();
|
||||
|
||||
@@ -160,7 +160,7 @@ describeEmbeddedPostgres("agent secret routes", () => {
|
||||
targetType: "agent",
|
||||
targetId: fixture.agentId,
|
||||
configPath: "access.API_ONLY_KEY",
|
||||
projectionClass: "class_2_runtime_only",
|
||||
projectionClass: "unclassified",
|
||||
});
|
||||
const projectBinding = await svc.createBinding({
|
||||
companyId: fixture.companyId,
|
||||
@@ -184,7 +184,7 @@ describeEmbeddedPostgres("agent secret routes", () => {
|
||||
const list = await request(createApp(fixture)).get("/api/agents/me/secrets");
|
||||
expect(list.status).toBe(200);
|
||||
expect(list.body.secrets).toEqual([
|
||||
expect.objectContaining({ key: "api_only_key", delivery: "api", projectionClass: "class_2_runtime_only" }),
|
||||
expect.objectContaining({ key: "api_only_key", delivery: "api", projectionClass: "unclassified" }),
|
||||
expect.objectContaining({ key: "env_only_key", delivery: "env" }),
|
||||
expect.objectContaining({ key: "project_key", delivery: "env" }),
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AdapterEnvironmentTestContext } from "@paperclipai/adapter-utils";
|
||||
import type { ServerAdapterModule } from "../adapters/index.js";
|
||||
|
||||
const mockAgentService = vi.hoisted(() => ({
|
||||
@@ -837,7 +838,7 @@ describe("agent test-environment route", () => {
|
||||
const { registerServerAdapter, getServerAdapter, unregisterServerAdapter } = await import("../adapters/index.js");
|
||||
const previous = getServerAdapter("claude_local");
|
||||
unregisterServerAdapter("claude_local");
|
||||
const cliProbeSpy = vi.fn(async () => ({
|
||||
const cliProbeSpy = vi.fn(async (_context: AdapterEnvironmentTestContext) => ({
|
||||
adapterType: "claude_local",
|
||||
status: "fail" as const,
|
||||
checks: [{ code: "adapter_command_missing", level: "error" as const, message: 'Command not found in PATH: "claude"' }],
|
||||
|
||||
@@ -57,7 +57,7 @@ const mockInstanceSettingsService = vi.hoisted(() => ({
|
||||
getGeneral: vi.fn(async () => ({ censorUsernameInLogs: false })),
|
||||
getExperimental: vi.fn(async () => ({ enableManagedSandboxOnly: false })),
|
||||
}));
|
||||
const mockRunClaudeLogin = vi.hoisted(() => vi.fn(async () => ({ ok: true })));
|
||||
const mockRunClaudeLogin = vi.hoisted(() => vi.fn(async (_input: { config: { env: Record<string, string> } }) => ({ ok: true })));
|
||||
|
||||
vi.mock("../services/index.js", () => ({
|
||||
agentService: () => mockAgentService,
|
||||
|
||||
@@ -1103,7 +1103,6 @@ describeEmbeddedPostgres("agent service Claude OAuth binding claim", () => {
|
||||
companyId: scope.companyId,
|
||||
ownerUserId: scope.ownerUserId,
|
||||
adapterType: "claude_local",
|
||||
environmentId: scope.environmentId,
|
||||
});
|
||||
const writer = createSetupTokenSecretWriter({ db });
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ if (!support.supported) console.warn(`Managed-account database tests unavailable
|
||||
const [connection] = await db.insert(toolConnections).values({ companyId: company.id, applicationId: application.id, uid: randomUUID(), name: `${provider} gateway`, transport: "mcp_remote", authKind: "none", credentialPolicy: "shared", status: "active", enabled: true, config: { sourceTemplateKey: provider, url: provider === "arcade" ? "https://api.arcade.dev/mcp/test" : "https://executor.example/mcp", managementUrl: "https://executor.example/team/integrations" } }).returning();
|
||||
await db.insert(connectionGrants).values({ companyId: company.id, connectionId: connection.id, kind: "organization", status: "active", isDefault: true });
|
||||
const names = provider === "executor" ? ["integrations"] : ["Notion.ListPages"];
|
||||
await db.insert(toolCatalogEntries).values(names.map(toolName => ({ companyId: company.id, connectionId: connection.id, name: toolName, toolName, versionHash: "v1", status: "active", riskLevel: "read" as const })));
|
||||
await db.insert(toolCatalogEntries).values(names.map(toolName => ({ companyId: company.id, connectionId: connection.id, name: toolName, toolName, versionHash: "v1", status: "active" as const, riskLevel: "read" as const })));
|
||||
let accounts = [{ integration: "notion", owner: "user", connection: "Work", lastHealth: { status: "healthy", checkedAt: Date.now() } }];
|
||||
let fail = false;
|
||||
let partial = false;
|
||||
@@ -88,7 +88,7 @@ if (!support.supported) console.warn(`Managed-account database tests unavailable
|
||||
expect((await f.access.listAggregatorApps(f.connection.id, f.actor)).apps).toHaveLength(0);
|
||||
});
|
||||
it("reports unsupported Executor inventory without trying arbitrary tools", async () => {
|
||||
const f = await fixture(); await sync(f); f.calls.length = 0; await db.update(toolCatalogEntries).set({ status: "inactive" }).where(eq(toolCatalogEntries.connectionId, f.connection.id));
|
||||
const f = await fixture(); await sync(f); f.calls.length = 0; await db.update(toolCatalogEntries).set({ status: "disabled" }).where(eq(toolCatalogEntries.connectionId, f.connection.id));
|
||||
expect((await f.access.syncAggregatorApps(f.connection.id, true, f.actor)).discovery.availability).toBe("unsupported");
|
||||
expect(f.calls).toHaveLength(0);
|
||||
expect((await f.access.listAggregatorApps(f.connection.id, f.actor)).apps[0].freshness).toBe("stale");
|
||||
|
||||
@@ -24,12 +24,12 @@ describe("personal AI credential preflight recovery", () => {
|
||||
expect(isAiAuthenticationBlocked({ ...run, livenessState: "blocked" })).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
it.each<[string, unknown]>([
|
||||
["tool credential", [{ ...missingClaudeKey, envKey: "GITHUB_TOKEN", configPath: "env.GITHUB_TOKEN" }]],
|
||||
["company secret", [{ ...missingClaudeKey, bindingType: "secret_ref" }]],
|
||||
["unknown error", [{ ...missingClaudeKey, errorCode: "user_secret_definition_missing" }]],
|
||||
["adapter field", [{ ...missingClaudeKey, configPath: "apiKey" }]],
|
||||
...["project", "environment", "routine", "plugin"].map(consumerType => [
|
||||
...["project", "environment", "routine", "plugin"].map((consumerType): [string, unknown] => [
|
||||
`${consumerType} binding`, [{ ...missingClaudeKey, consumerType }],
|
||||
]),
|
||||
["different agent", [{ ...missingClaudeKey, consumerId: "another-agent" }]],
|
||||
@@ -44,7 +44,7 @@ describe("personal AI credential preflight recovery", () => {
|
||||
});
|
||||
|
||||
it("only classifies a failed preflight as an inline wait after the card is persisted", () => {
|
||||
const input = { ...failure([missingClaudeKey]), runStatus: "failed", issue: { status: "blocked" },
|
||||
const input = { ...failure([missingClaudeKey]), runStatus: "failed", issue: { status: "blocked", title: "Blocked task", description: null },
|
||||
stdoutExcerpt: null, stderrExcerpt: null, error: null, continuationAttempt: 0, evidence: null };
|
||||
expect(classifyRunLiveness(input).livenessState).toBe("failed");
|
||||
expect(classifyRunLiveness({ ...input, authenticationRepairRequested: true })).toMatchObject({
|
||||
|
||||
@@ -136,7 +136,7 @@ describe("managed AI connections", () => {
|
||||
displayName: "Bootstrap", description: "Bootstrap fixture", author: "Test", categories: ["automation"],
|
||||
capabilities: ["environment.drivers.register"], entrypoints: { worker: "worker.js" },
|
||||
environmentDrivers: [{ driverKey: "tasks", displayName: "Tasks", supportsTasks: true, configSchema: { type: "object" } }] } });
|
||||
const [environment] = await db.insert(environments).values({ companyId, name: "Agent computer", driver: "plugin",
|
||||
const [environment] = await db.insert(environments).values({ name: "Agent computer", driver: "plugin",
|
||||
config: { pluginKey, driverKey: "tasks", driverConfig: {} } }).returning();
|
||||
const token = "github_pat_fixture_plugin_bootstrap";
|
||||
const account = await service.save(companyId, owner, { provider: "github", method: "api_key", ownership: "personal", name: "Bootstrap", apiKey: token, agentIds: [], allAgents: false }, token);
|
||||
@@ -300,7 +300,7 @@ describe("managed AI connections", () => {
|
||||
await db.insert(principalPermissionGrants).values([owner, audience].map(principalId => ({ companyId, principalId, principalType: "user", permissionKey: "agents:create" })));
|
||||
const token = "github_pat_fixture_shared_discovery";
|
||||
const account = await service.save(companyId, owner, { provider: "github", method: "api_key", ownership: "shared", name: "Shared discovery", apiKey: token, agentIds: [], allAgents: false }, token);
|
||||
await db.insert(connectionGrantMembers).values([owner, audience].map(subjectId => ({ companyId, grantId: account.grantId, subjectType: "user", subjectId })));
|
||||
await db.insert(connectionGrantMembers).values([owner, audience].map(subjectId => ({ companyId, grantId: account.grantId, subjectType: "user" as const, subjectId })));
|
||||
const binding = { provider: "github", method: "api_key", mode: "shared", ...account };
|
||||
const probe = vi.spyOn(copilotProbe, "probeCopilotConnection").mockResolvedValue({ status: "verified", version: "1.0.88", profileDigest: "fixture-profile", promptSent: false, models: [{ id: "gpt-5.6-luna", label: "GPT" }] });
|
||||
const app = express(); app.use(express.json());
|
||||
@@ -1338,7 +1338,7 @@ describe("managed AI connections", () => {
|
||||
agentIds: [], allAgents: true,
|
||||
routing: { kind: "local", protocol, baseUrl: "http://127.0.0.1:9000/v1", auth: "none", models: [] },
|
||||
}, "");
|
||||
const binding = { provider, method: "api_key", mode: "connection", connectionId: account.connectionId, grantId: account.grantId } as const;
|
||||
const binding = { provider, method: "api_key", mode: "delegated", connectionId: account.connectionId, grantId: account.grantId } as const;
|
||||
const config = { model: "fixture-model", env: { OPENAI_API_KEY: "host-key", ANTHROPIC_API_KEY: "host-key" } };
|
||||
const first = await prepareManagedAiRuntime(db, { ...input, adapterType, responsibleUserId: "alice", binding, config });
|
||||
const second = await prepareManagedAiRuntime(db, { ...input, adapterType, responsibleUserId: "alice", binding, config });
|
||||
@@ -1357,7 +1357,7 @@ describe("managed AI connections", () => {
|
||||
}, "fixture-grok-key");
|
||||
const taskKey = `grok-history-${randomUUID()}`;
|
||||
const runInput = { ...input, taskKey, adapterType: "grok_local", responsibleUserId: "alice", config: {},
|
||||
binding: { provider: "xai", method: "api_key", mode: "connection", connectionId: account.connectionId, grantId: account.grantId } as const };
|
||||
binding: { provider: "xai", method: "api_key", mode: "delegated", connectionId: account.connectionId, grantId: account.grantId } as const };
|
||||
const first = await prepareManagedAiRuntime(db, runInput);
|
||||
const scope = createHash("sha256").update(JSON.stringify([
|
||||
companyId, agentId, taskKey, first.sessionIdentity,
|
||||
@@ -1597,12 +1597,12 @@ console.log(JSON.stringify({ type: "result", subtype: "success", result: "hello"
|
||||
const account = await service.save(companyId, "alice", { provider: "anthropic", method: "subscription", ownership: "shared", name: "Shared usage account", loginSessionId: "fixture", agentIds: [], allAgents: true }, "shared-usage-secret");
|
||||
const selected = { provider: "anthropic", method: "subscription", mode: "shared", ...account } as const;
|
||||
const tools = toolAccessService(db);
|
||||
await tools.replaceConnectionGrantMembers(account.connectionId, account.grantId, ["alice"], { userId: "alice" });
|
||||
await tools.replaceConnectionGrantMembers(account.connectionId, account.grantId, ["alice"], { actorType: "user", actorId: "alice" });
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response(JSON.stringify({ five_hour: { utilization: 100 }, extra_usage: { is_enabled: false } })));
|
||||
try {
|
||||
await expect(service.probeUsage(companyId, "bob", account.connectionId)).rejects.toThrow("not found");
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
await tools.replaceConnectionGrantMembers(account.connectionId, account.grantId, ["bob"], { userId: "alice" });
|
||||
await tools.replaceConnectionGrantMembers(account.connectionId, account.grantId, ["bob"], { actorType: "user", actorId: "alice" });
|
||||
for (const config of [{ adapterType: "claude_local" }, { adapterType: "paperclip_runner", runnerProvider: "acpx", acpxAgent: "claude" }]) {
|
||||
const row = await service.select({ companyId, userId: "bob", agentId, binding: selected, ...config });
|
||||
expect(await service.probeUsage(companyId, "bob", row.connection.id, row.grant.id)).toMatchObject({ status: "ok", overage: { available: false }, limits: [{ limitReached: true }, { allowed: false }] });
|
||||
@@ -1673,7 +1673,7 @@ console.log(JSON.stringify({ type: "result", subtype: "success", result: "hello"
|
||||
it("reconnects JSONB routing without changing its identity or access", async () => {
|
||||
const routing = { kind: "gateway", protocol: "responses", auth: "bearer", baseUrl: "https://gateway.example/v1", models: [{ id: "gateway-model" }] } as const;
|
||||
const input = { provider: "openai", method: "api_key", name: "Reconnect gateway", ownership: "personal", allAgents: false, agentIds: [agentId], routing: { ...routing, models: [...routing.models] } } as const;
|
||||
const saved = await service.save(companyId, "alice", { ...input, agentIds: [...input.agentIds] }, "old-gateway-credential");
|
||||
const saved = await service.save(companyId, "alice", { ...input, agentIds: [...input.agentIds], routing: { ...routing, models: [...routing.models] } }, "old-gateway-credential");
|
||||
const [stored] = await db.select().from(toolConnections).where(eq(toolConnections.id, saved.connectionId));
|
||||
expect(stored!.config.ai).toMatchObject({ routing });
|
||||
expect(stored!.config.sourceTemplateKey).toBe("responses-api");
|
||||
@@ -1788,7 +1788,7 @@ console.log(JSON.stringify({ type: "result", subtype: "success", result: "hello"
|
||||
const runId = randomUUID();
|
||||
const vault = secretService(db);
|
||||
const definition = await vault.createUserSecretDefinition(companyId, {
|
||||
key: `ANTHROPIC_API_KEY.${randomUUID()}`, name: "ANTHROPIC_API_KEY for onboarding",
|
||||
key: `ANTHROPIC_API_KEY.${randomUUID()}`, name: "ANTHROPIC_API_KEY for onboarding", provider: "local_encrypted",
|
||||
}, { userId: "alice" });
|
||||
const ownerSecret = await vault.createCurrentUserSecretValue(companyId, "alice", {
|
||||
definitionId: definition.id, value: "fixture-original-owner-key",
|
||||
@@ -1877,8 +1877,8 @@ console.log(JSON.stringify({ type: "result", subtype: "success", result: "hello"
|
||||
}
|
||||
expect(subEnv.HOME).not.toBe(apiEnvValues.HOME);
|
||||
expect(subRun.identity).not.toBe(apiRun.identity);
|
||||
expect(subRun.config.model).toBe(bot.config.model);
|
||||
expect(apiRun.config.model).toBe(bot.config.model);
|
||||
expect((subRun.config as Record<string, unknown>).model).toBe(bot.config.model);
|
||||
expect((apiRun.config as Record<string, unknown>).model).toBe(bot.config.model);
|
||||
expect(bot).toEqual(original);
|
||||
expect(aiConnectionBindingSchema.parse(bot.binding)).toEqual(bot.binding);
|
||||
} finally { await Promise.all([subRun.cleanup(), apiRun.cleanup()]); }
|
||||
@@ -2005,16 +2005,16 @@ console.log(JSON.stringify({ type: "result", subtype: "success", result: "hello"
|
||||
const shared = await create("alice", "Engineering", "shared");
|
||||
const sharedBinding = { ...binding, mode: "shared" as const, ...shared };
|
||||
const tools = toolAccessService(db);
|
||||
await tools.replaceConnectionGrantMembers(shared.connectionId, shared.grantId, ["alice"], { userId: "alice" });
|
||||
await tools.replaceConnectionGrantMembers(shared.connectionId, shared.grantId, ["alice"], { actorType: "user", actorId: "alice" });
|
||||
const rejection = await service.select({ ...input, userId: "bob", binding: sharedBinding }).catch(error => error);
|
||||
expect(rejection).toMatchObject({ status: 403, message: "This credential is not shared with the responsible user" });
|
||||
expect(readAiConnectionConfigurationFailure(rejection)).toBe("ai_connection_credential_not_shared");
|
||||
expect((await service.list(companyId, "bob", agentId)).some(account => account.id === shared.connectionId)).toBe(false);
|
||||
await tools.replaceConnectionGrantMembers(shared.connectionId, shared.grantId, ["bob"], { userId: "alice" });
|
||||
await tools.replaceConnectionGrantMembers(shared.connectionId, shared.grantId, ["bob"], { actorType: "user", actorId: "alice" });
|
||||
expect((await service.select({ ...input, userId: "bob", binding: sharedBinding })).grant.id).toBe(shared.grantId);
|
||||
expect((await service.list(companyId, "bob", agentId)).some(account => account.id === shared.connectionId)).toBe(true);
|
||||
await expect(service.select({ ...input, userId: "alice", binding: sharedBinding })).rejects.toThrow("not shared");
|
||||
await tools.replaceConnectionGrantMembers(shared.connectionId, shared.grantId, [], { userId: "alice" });
|
||||
await tools.replaceConnectionGrantMembers(shared.connectionId, shared.grantId, [], { actorType: "user", actorId: "alice" });
|
||||
for (const userId of ["alice", "bob"]) {
|
||||
expect((await service.select({ ...input, userId, binding: sharedBinding })).grant.id).toBe(shared.grantId);
|
||||
}
|
||||
@@ -2062,7 +2062,7 @@ console.log(JSON.stringify({ type: "result", subtype: "success", result: "hello"
|
||||
const ae = a.config.env as Record<string,string>, be = b.config.env as Record<string,string>;
|
||||
expect(ae.ANTHROPIC_API_KEY).toBe("fixture-Alice second"); expect(be.ANTHROPIC_API_KEY).toBe("fixture-Bob first");
|
||||
expect(ae.HOME).not.toBe(be.HOME); expect(a.identity).not.toBe(b.identity);
|
||||
expect(a.config.model).toBe("unchanged-model"); expect(config.env.ANTHROPIC_API_KEY).toBe("project-never-use");
|
||||
expect((a.config as Record<string, unknown>).model).toBe("unchanged-model"); expect(config.env.ANTHROPIC_API_KEY).toBe("project-never-use");
|
||||
await Promise.all([a.cleanup(), b.cleanup()]); await expect(access(ae.HOME)).rejects.toThrow();
|
||||
});
|
||||
it("blocks missing identity, incompatible providers, and cross-company explicit selections", async () => {
|
||||
@@ -2585,7 +2585,7 @@ console.log(JSON.stringify({ type: "result", subtype: "success", result: "hello"
|
||||
const selectionInput = { ...input, agentId: futureAgentId, userId: "alice", binding: selected };
|
||||
await expect(service.select(selectionInput)).rejects.toThrow("not permitted for this agent");
|
||||
const run = await prepareManagedAiRuntime(db, { companyId, agentId: futureAgentId, responsibleUserId: "alice", adapterType: "claude_local", binding: selected, config: { cwd: home, model: "same-model" }, allowUninstalledShared: true });
|
||||
expect(run.config.model).toBe("same-model");
|
||||
expect((run.config as Record<string, unknown>).model).toBe("same-model");
|
||||
await run.cleanup();
|
||||
await db.insert(connectionGrantMembers).values({ companyId, grantId: account.grantId, subjectType: "user", subjectId: "bob" });
|
||||
await expect(service.select({ ...selectionInput, allowUninstalledShared: true })).rejects.toThrow("not shared with the responsible user");
|
||||
@@ -2629,7 +2629,7 @@ console.log(JSON.stringify({ type: "result", subtype: "success", result: "hello"
|
||||
await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId: id, status: "failed", errorCode: "acpx_auth_required", responsibleUserId: "alice", contextSnapshot: { issueId } });
|
||||
interactionId = (await connectionIntentService(db).requestForRunAuthFailure(runId))!.interactionId!;
|
||||
}
|
||||
const target = { kind: "remote", transport: "sandbox", remoteCwd: "/workspace", providerKey: "daytona", runner: { execute: vi.fn(async () => ({ exitCode: 0, stdout: "", stderr: "", timedOut: false })) } } as const;
|
||||
const target = { kind: "remote", transport: "sandbox", remoteCwd: "/workspace", providerKey: "daytona", runner: { execute: vi.fn(async () => ({ exitCode: 0, signal: null, stdout: "", stderr: "", timedOut: false, pid: null, startedAt: null })) } } as const;
|
||||
const resolveTarget = vi.spyOn(targetModule, "resolveEnvironmentExecutionTarget").mockResolvedValue(target);
|
||||
const release = vi.fn(async () => undefined);
|
||||
const acquire = vi.fn(async () => ({ lease: { id: randomUUID(), provider: "daytona", providerLeaseId: "test-sandbox", metadata: {} }, leaseContext: {} }));
|
||||
|
||||
@@ -52,7 +52,7 @@ it("reconnect detaches an indexed credential without changing unadopted legacy a
|
||||
const input = { companyId, agentId, responsibleUserId: owner, adapterType: "claude_local", binding: { provider: "anthropic", method: "api_key", mode: "delegated", connectionId: connection.id, grantId: connection.grantId } as const, config: {}, allowLegacyValidation: true };
|
||||
const before = await prepareManagedAiRuntime(db, input);
|
||||
await before.cleanup();
|
||||
const resolve = () => vault.resolveUserSecretValue(companyId, { definitionId: definition.id, responsibleUserId: owner, required: true, version: "latest" }, { companyId, responsibleUserId: owner, actorType: "system" });
|
||||
const resolve = () => vault.resolveUserSecretValue(companyId, { definitionId: definition.id, responsibleUserId: owner, required: true, version: "latest" }, { consumerType: "agent", consumerId: agentId, responsibleUserId: owner, actorType: "system" });
|
||||
expect((await resolve())?.value).toBe("fixture-original");
|
||||
await service.save(companyId, owner, { provider: "anthropic", method: "api_key", name: connection.name, ownership: "personal", agentIds: [], allAgents: true, connectionId: connection.id, apiKey: "fixture-new-account" }, "fixture-new-account");
|
||||
expect((await resolve())?.value).toBe("fixture-original");
|
||||
@@ -155,7 +155,7 @@ it("isolates sign-in and refresh from the host, survives restart, and completes
|
||||
expect(await readFile(path.join(hostHome, "auth.json"), "utf8")).toBe(auth("legacy"));
|
||||
const next = await prepareManagedAiRuntime(db, input);
|
||||
expect(JSON.parse(await readFile(path.join(String(next.config.env.CODEX_HOME), "auth.json"), "utf8")).tokens.refresh_token).toBe("refresh-rotated-independent");
|
||||
expect(next.config.model).toBe(input.config.model);
|
||||
expect((next.config as Record<string, unknown>).model).toBe(input.config.model);
|
||||
await next.cleanup();
|
||||
// Cancellation after successful completion must not revoke the saved account.
|
||||
await login.cancel(companyId, owner, attempt.sessionId);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { access, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
||||
import { promises as fs } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest";
|
||||
import {
|
||||
agents, authUsers, closeRegisteredClients, companies, companyMemberships, companySecrets, companySecretVersions,
|
||||
connectionGrants, createDb, userSecretDefinitions,
|
||||
@@ -36,7 +36,7 @@ describeDatabase("Google AI connection authority with real PostgreSQL", () => {
|
||||
let db: ReturnType<typeof createDb>;
|
||||
let embedded: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | undefined;
|
||||
let isolatedHome: ReturnType<typeof createIsolatedTestHome>;
|
||||
let request: ReturnType<typeof vi.spyOn<typeof globalThis, "fetch">>;
|
||||
let request: MockInstance<typeof fetch>;
|
||||
|
||||
beforeAll(async () => {
|
||||
isolatedHome = createIsolatedTestHome();
|
||||
|
||||
@@ -190,7 +190,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
it("wakes from batch-imported PR work products", async () => {
|
||||
const target = await issue(), approval = await pending(target.id);
|
||||
await start(); await quiet();
|
||||
await workProductService(db).createManyForImport([{ companyId, issueId: target.id, type: "pull_request", provider: "github", title: "Imported PR", status: "open", reviewState: "none", isPrimary: false, healthStatus: "unknown" }]);
|
||||
await workProductService(db).createManyForImport([{ companyId, issueId: target.id, projectId: null, type: "pull_request", provider: "github", externalId: null, title: "Imported PR", url: null, status: "open", reviewState: "none", isPrimary: false, healthStatus: "unknown", summary: null, metadata: null, executionWorkspaceId: null, runtimeServiceId: null, createdByRunId: null, sourceTrust: null }]);
|
||||
await seeded(approval.id); await quiet();
|
||||
});
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ const delay = DEFAULT_DECISION_ARCHIVE_DAYS * 86_400_000;
|
||||
const rows = await db.insert(issues).values(Array.from({ length: 100 }, () => ({ companyId,
|
||||
title: randomUUID(), status: "in_review", assigneeAgentId: agentId, updatedAt: old() }))).returning();
|
||||
await db.insert(issueThreadInteractions).values(rows.map(row => ({ companyId, issueId: row.id,
|
||||
kind: "request_confirmation", addresseeAgentId: agentId, payload: { version: 1, prompt: "Agent verdict" } })));
|
||||
kind: "request_confirmation" as const, addresseeAgentId: agentId, payload: { version: 1 as const, prompt: "Agent verdict" } })));
|
||||
statements.length = 0;
|
||||
const page = await attentionRetentionService(db).load({ companyId, sourceKind: "review" });
|
||||
expect(page.rows).toEqual([]); expect(page.complete).toBe(false);
|
||||
@@ -278,11 +278,11 @@ const delay = DEFAULT_DECISION_ARCHIVE_DAYS * 86_400_000;
|
||||
it("batches 100 blocker graphs with exact terminal timestamps and fresh terminal visibility", async () => {
|
||||
const roots = await db.insert(issues).values(Array.from({ length: 100 }, () => ({ companyId,
|
||||
title: randomUUID(), status: "blocked", updatedAt: old(), blockedTransitionAt: old(),
|
||||
unblockDescriptor: { owner: "board", action: "Pick an owner" } }))).returning();
|
||||
unblockDescriptor: { owner: "board" as const, action: "Pick an owner" } }))).returning();
|
||||
const terminals = await db.insert(issues).values(Array.from({ length: 100 }, () => ({ companyId,
|
||||
title: randomUUID(), status: "backlog", assigneeAgentId: agentId, updatedAt: old() }))).returning();
|
||||
await db.insert(issueRelations).values(roots.map((root, index) => ({ companyId,
|
||||
issueId: terminals[index].id, relatedIssueId: root.id, type: "blocks" })));
|
||||
issueId: terminals[index].id, relatedIssueId: root.id, type: "blocks" as const })));
|
||||
await db.execute(sql`update issues set updated_at = updated_at + interval '0.000321 seconds' where id = ${terminals[99].id}::uuid`);
|
||||
const service = attentionRetentionService(db), scope = { companyId, sourceKind: "blocker_attention" as const };
|
||||
statements.length = 0;
|
||||
@@ -318,7 +318,7 @@ const delay = DEFAULT_DECISION_ARCHIVE_DAYS * 86_400_000;
|
||||
{ companyId, issueId: middle.id, relatedIssueId: root.id, type: "blocks" },
|
||||
{ companyId, issueId: terminal.id, relatedIssueId: middle.id, type: "blocks" },
|
||||
...heavyNodes.map((node, index) => ({ companyId, issueId: node.id,
|
||||
relatedIssueId: limit === "nodes" || index === 0 ? heavy.id : heavyNodes[index - 1].id, type: "blocks" })),
|
||||
relatedIssueId: limit === "nodes" || index === 0 ? heavy.id : heavyNodes[index - 1].id, type: "blocks" as const })),
|
||||
]);
|
||||
const owner = issueService(db);
|
||||
const expected = (await owner.listBlockerAttention(companyId, [root])).get(root.id)!;
|
||||
@@ -350,7 +350,7 @@ const delay = DEFAULT_DECISION_ARCHIVE_DAYS * 86_400_000;
|
||||
const cycle = await insertIssue("blocked");
|
||||
const leaf = await insertIssue("backlog", { assigneeAgentId: agentId });
|
||||
await db.insert(issueRelations).values([
|
||||
...chain.map((node, index) => ({ companyId, issueId: node.id, relatedIssueId: index ? chain[index - 1].id : root.id, type: "blocks" })),
|
||||
...chain.map((node, index) => ({ companyId, issueId: node.id, relatedIssueId: index ? chain[index - 1].id : root.id, type: "blocks" as const })),
|
||||
{ companyId, issueId: leaf.id, relatedIssueId: cycle.id, type: "blocks" },
|
||||
{ companyId, issueId: cycle.id, relatedIssueId: leaf.id, type: "blocks" },
|
||||
]);
|
||||
@@ -378,7 +378,7 @@ const delay = DEFAULT_DECISION_ARCHIVE_DAYS * 86_400_000;
|
||||
});
|
||||
const listed = await issueService(db).list(companyId, { status: "blocked", includeBlockedBy: true });
|
||||
expect(listed).toHaveLength(roots.length);
|
||||
for (const root of listed) expect(root.blockerAttention).toMatchObject({ state: "needs_attention", terminalBlockerIssueId: root.id });
|
||||
for (const root of listed) expect((root as typeof root & { blockerAttention?: unknown }).blockerAttention).toMatchObject({ state: "needs_attention", terminalBlockerIssueId: root.id });
|
||||
const feed = await attentionService(db).list(companyId, { all: true, allowUnscopedAll: true });
|
||||
const visible = new Set(feed.items.filter(item => item.sourceKind === "blocker_attention").map(item => item.subject.id));
|
||||
expect(visible).toEqual(new Set(roots.map(root => root.id)));
|
||||
@@ -457,7 +457,7 @@ const delay = DEFAULT_DECISION_ARCHIVE_DAYS * 86_400_000;
|
||||
const [run] = await db.insert(heartbeatRuns).values({ companyId, agentId, status: "running" }).returning();
|
||||
await db.update(issues).set({ executionRunId: run.id }).where(eq(issues.id, child.id));
|
||||
} else if (kind === "confirmation") {
|
||||
await db.insert(issueThreadInteractions).values({ companyId, issueId: child.id, kind: "request_confirmation", status: "pending", payload: { prompt: "Approve?" } });
|
||||
await db.insert(issueThreadInteractions).values({ companyId, issueId: child.id, kind: "request_confirmation", status: "pending", payload: { version: 1, prompt: "Approve?" } });
|
||||
}
|
||||
parameterCounts.length = 0;
|
||||
const batch = await issueService(db).listBlockerAttentionBatch(companyId, [root]);
|
||||
@@ -495,7 +495,7 @@ const delay = DEFAULT_DECISION_ARCHIVE_DAYS * 86_400_000;
|
||||
const [run] = await db.insert(heartbeatRuns).values({ companyId, agentId, status: "running" }).returning();
|
||||
await db.update(issues).set({ executionRunId: run.id }).where(eq(issues.id, live.id));
|
||||
} else if (kind === "confirmation") await db.insert(issueThreadInteractions).values({ companyId, issueId: live.id,
|
||||
kind: "request_confirmation", status: "pending", payload: { prompt: "Still waiting" } });
|
||||
kind: "request_confirmation", status: "pending", payload: { version: 1, prompt: "Still waiting" } });
|
||||
}
|
||||
statements.length = 0; parameterCounts.length = 0;
|
||||
captureBlockerPlan = kind === "completed";
|
||||
@@ -687,13 +687,13 @@ const delay = DEFAULT_DECISION_ARCHIVE_DAYS * 86_400_000;
|
||||
let query!: Parameters<typeof db.execute>[0], returned = 0;
|
||||
await db.transaction(async tx => {
|
||||
const execute = tx.execute.bind(tx);
|
||||
vi.spyOn(tx, "execute").mockImplementation(async statement => {
|
||||
vi.spyOn(tx, "execute").mockImplementation((async (statement: Parameters<typeof execute>[0]) => {
|
||||
const result = await execute(statement);
|
||||
if (new PgDialect().sqlToQuery(statement as SQL).sql.includes("with recursive affected")) {
|
||||
query = statement; returned = result.length;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}) as unknown as typeof execute);
|
||||
await notifyIssueAttentionRetentionWork(tx, { companyId, issueId: leaf.id });
|
||||
});
|
||||
expect(returned).toBe(101);
|
||||
@@ -804,7 +804,7 @@ const delay = DEFAULT_DECISION_ARCHIVE_DAYS * 86_400_000;
|
||||
const [decision] = await db.insert(decisions).values({ companyId, originAgentId: agentId, originIssueId: origin.id, originRunId: run.id,
|
||||
title: "Decision", body: "Choose", options: [], signedSpec: "fixture", targetSnapshots: {}, expiresAt: new Date(Date.now() + 86_400_000), updatedAt: old() }).returning();
|
||||
const [recovery] = await db.insert(issueRecoveryActions).values({ companyId, sourceIssueId: origin.id, kind: "execution", ownerType: "board", cause: "fixture", fingerprint: "fixture", nextAction: "Choose", updatedAt: old() }).returning();
|
||||
const [interaction] = await db.insert(issueThreadInteractions).values({ companyId, issueId: origin.id, kind: "ask_user_questions", payload: { questions: [] }, updatedAt: old() }).returning();
|
||||
const [interaction] = await db.insert(issueThreadInteractions).values({ companyId, issueId: origin.id, kind: "ask_user_questions", payload: { version: 1, questions: [] }, updatedAt: old() }).returning();
|
||||
await start(); await quiet();
|
||||
await issueService(db).update(origin.id, { hiddenAt: null });
|
||||
for (const [kind, id] of [["decision", decision.id], ["recovery_action", recovery.id], ["failed_run", run.id], ["issue_thread_interaction", interaction.id]]) await archived(kind, id);
|
||||
@@ -933,10 +933,10 @@ const delay = DEFAULT_DECISION_ARCHIVE_DAYS * 86_400_000;
|
||||
await db.insert(decisions).values({ companyId, originAgentId: agentId, originIssueId: origin.id, originRunId: run.id,
|
||||
title: "Decision", body: "Choose", options: [], signedSpec: "fixture", targetSnapshots: {}, expiresAt: new Date(Date.now() + 86_400_000), updatedAt: old() });
|
||||
await db.insert(issueThreadInteractions).values([
|
||||
{ companyId, issueId: origin.id, kind: "ask_user_questions", payload: { questions: [] }, updatedAt: old() },
|
||||
{ companyId, issueId: origin.id, kind: "ask_user_questions", payload: { questions: [] }, addresseeAgentId: agentId, updatedAt: old() },
|
||||
{ companyId, issueId: origin.id, kind: "ask_user_questions", payload: { questions: [] }, addresseeUserId: "board", updatedAt: old() },
|
||||
{ companyId, issueId: hidden.id, kind: "ask_user_questions", payload: { questions: [] }, updatedAt: old() },
|
||||
{ companyId, issueId: origin.id, kind: "ask_user_questions", payload: { version: 1, questions: [] }, updatedAt: old() },
|
||||
{ companyId, issueId: origin.id, kind: "ask_user_questions", payload: { version: 1, questions: [] }, addresseeAgentId: agentId, updatedAt: old() },
|
||||
{ companyId, issueId: origin.id, kind: "ask_user_questions", payload: { version: 1, questions: [] }, addresseeUserId: "board", updatedAt: old() },
|
||||
{ companyId, issueId: hidden.id, kind: "ask_user_questions", payload: { version: 1, questions: [] }, updatedAt: old() },
|
||||
]);
|
||||
await db.insert(issueRecoveryActions).values({ companyId, sourceIssueId: origin.id, kind: "execution", ownerType: "board", cause: "fixture", fingerprint: "fixture", nextAction: "Choose", updatedAt: old() });
|
||||
const [policy] = await db.insert(budgetPolicies).values({ companyId, scopeType: "company", scopeId: companyId, windowKind: "calendar_month_utc", amount: 100 }).returning();
|
||||
@@ -1024,16 +1024,16 @@ const delay = DEFAULT_DECISION_ARCHIVE_DAYS * 86_400_000;
|
||||
{ companyId, name: "Unhealthy", role: "engineer", status: "active" },
|
||||
]).returning();
|
||||
await db.update(agents).set({ reportsTo: manager.id }).where(eq(agents.id, unhealthy.id));
|
||||
const common = { companyId, issueId: origin.id, kind: "request_confirmation", payload: { prompt: "Approve?" }, createdAt: old(), updatedAt: old() };
|
||||
const common = { companyId, issueId: origin.id, kind: "request_confirmation", payload: { version: 1, prompt: "Approve?" }, createdAt: old(), updatedAt: old() };
|
||||
const [older, newest, independent] = await db.insert(issueThreadInteractions).values([
|
||||
common, { ...common, createdAt: new Date(old().getTime() + 1000) },
|
||||
{ ...common, payload: { prompt: "Independent", qualityReview: { artifactKey: "copy" } } },
|
||||
{ ...common, createdAt: new Date(), addresseeUserId: "board" },
|
||||
{ ...common, addresseeAgentId: agentId },
|
||||
{ ...common, issueId: hidden.id },
|
||||
{ ...common, issueId: conversation.id, kind: "ask_user_questions", payload: { questions: [] } },
|
||||
{ ...common, issueId: conversation.id, kind: "ask_user_questions", payload: { version: 1, questions: [] } },
|
||||
{ ...common, issueId: conversation.id, addresseeAgentId: paused.id },
|
||||
{ ...common, issueId: conversation.id, addresseeAgentId: unhealthy.id, kind: "ask_user_questions", payload: { questions: [] } },
|
||||
{ ...common, issueId: conversation.id, addresseeAgentId: unhealthy.id, kind: "ask_user_questions", payload: { version: 1, questions: [] } },
|
||||
] as typeof issueThreadInteractions.$inferInsert[]).returning();
|
||||
const feed = await attentionService(db).list(companyId, { all: true, allowUnscopedAll: true });
|
||||
const expected = [...new Set(feed.items.map(item => `${item.sourceKind}:${item.subject.id}`))].sort();
|
||||
@@ -1048,7 +1048,7 @@ const delay = DEFAULT_DECISION_ARCHIVE_DAYS * 86_400_000;
|
||||
it("excludes maintained reviews until a pending approval creates human attention", async () => {
|
||||
const row = await insertIssue("in_review", { assigneeAgentId: agentId });
|
||||
const [interaction] = await db.insert(issueThreadInteractions).values({ companyId, issueId: row.id,
|
||||
kind: "request_confirmation", addresseeAgentId: agentId, payload: { prompt: "Agent verdict" } }).returning();
|
||||
kind: "request_confirmation", addresseeAgentId: agentId, payload: { version: 1, prompt: "Agent verdict" } }).returning();
|
||||
expect((await loadAll()).some(entry => entry.sourceKind === "review")).toBe(false);
|
||||
const [approval] = await db.insert(approvals).values({ companyId, type: "hire_agent", payload: {}, status: "pending" }).returning();
|
||||
await db.insert(issueApprovals).values({ companyId, issueId: row.id, approvalId: approval.id });
|
||||
|
||||
@@ -1545,8 +1545,8 @@ describeEmbeddedPostgres("attention service", () => {
|
||||
const issueId = await insertIssue({ companyId, identifier: "ATQ-1", title: "Review copy and visual", status: "in_review", assigneeAgentId: workerId });
|
||||
const ids = [randomUUID(), randomUUID(), randomUUID(), randomUUID()];
|
||||
await db.insert(issueThreadInteractions).values(ids.map((id, index) => ({
|
||||
id, companyId, issueId, kind: "request_confirmation", status: "pending", continuationPolicy: "wake_assignee", createdByAgentId: workerId,
|
||||
payload: { version: 1, prompt: "Approve output?", ...(index < 2 ? { qualityReview: { key: index === 0 ? "copy" : "visual", requirementHash: "a".repeat(64) } } : {}) },
|
||||
id, companyId, issueId, kind: "request_confirmation" as const, status: "pending" as const, continuationPolicy: "wake_assignee" as const, createdByAgentId: workerId,
|
||||
payload: { version: 1 as const, prompt: "Approve output?", ...(index < 2 ? { qualityReview: { key: index === 0 ? "copy" : "visual", requirementHash: "a".repeat(64) } } : {}) },
|
||||
createdAt: new Date(Date.UTC(2026, 6, 9, 12, index)),
|
||||
})));
|
||||
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
|
||||
|
||||
@@ -18,7 +18,7 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
db = createDb(database.connectionString, { maxConnections: 1 });
|
||||
client = db.$client;
|
||||
client.options.transform.value.from = (value, column) => {
|
||||
if (recording && column.type === 3802 && value !== null) jsonBytes.push(Buffer.byteLength(JSON.stringify(value)));
|
||||
if (recording && column?.type === 3802 && value !== null) jsonBytes.push(Buffer.byteLength(JSON.stringify(value)));
|
||||
return value;
|
||||
};
|
||||
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Request } from "express";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { HttpError } from "../errors.js";
|
||||
import { assertBoardOrgAccess, assertCompanyAccess, hasBoardOrgAccess, hasCompanyAccess } from "../routes/authz.js";
|
||||
@@ -9,7 +10,7 @@ function makeReq(input: {
|
||||
return {
|
||||
method: input.method ?? "GET",
|
||||
actor: input.actor,
|
||||
} as Express.Request;
|
||||
} as unknown as Request;
|
||||
}
|
||||
|
||||
describe("assertCompanyAccess", () => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Request } from "express";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildActorSecretContext } from "../routes/authz.js";
|
||||
|
||||
function makeReq(actor: Express.Request["actor"]) {
|
||||
return { method: "POST", actor } as Express.Request;
|
||||
return { method: "POST", actor } as unknown as Request;
|
||||
}
|
||||
|
||||
describe("buildActorSecretContext", () => {
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("POST /bootstrap/claim", () => {
|
||||
expect(claimFirstInstanceAdminMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
it.each<[Record<string, unknown>, string]>([
|
||||
[{ type: "none", source: "none" }, "anonymous caller"],
|
||||
[{ type: "agent", source: "agent_key", agentId: "agent-1" }, "agent key"],
|
||||
[{ type: "board", source: "board_key", userId: "user-1" }, "board API key"],
|
||||
|
||||
@@ -214,7 +214,7 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
if (methodKey.endsWith("oauth")) await f.signIn(connected.connectionId);
|
||||
else await f.service.reconnectGalleryApp(connected.connectionId, f.company.id, { credentialValues: { "credentials.authorization": f.accessToken } }, actor);
|
||||
const afterReconnect = await f.service.refreshCatalog(connected.connectionId, actor, { enableAllByDefault: true });
|
||||
expect(afterReconnect.connection.config.quarantineNewEntries).toBe(true);
|
||||
expect(afterReconnect.connection.config?.quarantineNewEntries).toBe(true);
|
||||
expect(afterReconnect.connection.transportConfig.quarantineNewEntries).toBe(true);
|
||||
expect(afterReconnect.catalog.filter(entry => ["sql_query", "provider_added_tool"].includes(entry.toolName)).every(entry => entry.status === "quarantined")).toBe(true);
|
||||
f.request.mockClear();
|
||||
|
||||
@@ -1165,10 +1165,10 @@ const actor = { actorType: "user" as const, actorId: "browser-reviewer" };
|
||||
const resizePath = path.replace("/viewer", "/viewport");
|
||||
const resizeSpy = vi
|
||||
.spyOn(browserUseViewports, "resize")
|
||||
.mockResolvedValue({ preset: "phone" });
|
||||
.mockResolvedValue({ preset: "phone", applied: true });
|
||||
expect(
|
||||
(await http(app).post(resizePath).send({ preset: "phone" })).body,
|
||||
).toEqual({ preset: "phone" });
|
||||
).toEqual({ preset: "phone", applied: true });
|
||||
expect(resizeSpy).toHaveBeenCalledTimes(1);
|
||||
const viewerId = randomUUID();
|
||||
resizeSpy.mockResolvedValueOnce({
|
||||
@@ -1457,7 +1457,7 @@ const actor = { actorType: "user" as const, actorId: "browser-reviewer" };
|
||||
});
|
||||
it.each(["default_allow", "tool_name"])("retains valid %s profile authorization at browser admission", async selector => {
|
||||
const f = await fixture();
|
||||
await db.update(toolProfiles).set({ status: "inactive" }).where(eq(toolProfiles.companyId, f.company.id));
|
||||
await db.update(toolProfiles).set({ status: "disabled" }).where(eq(toolProfiles.companyId, f.company.id));
|
||||
const [profile] = await db.insert(toolProfiles).values({ companyId: f.company.id, name: "Scoped browser access",
|
||||
profileKey: randomUUID(), defaultAction: selector === "default_allow" ? "allow" : "deny" }).returning();
|
||||
await db.insert(toolProfileBindings).values({ companyId: f.company.id, profileId: profile.id, targetType: "agent", targetId: f.agent.id });
|
||||
|
||||
@@ -35,7 +35,7 @@ afterEach(async () => {
|
||||
});
|
||||
function setup() {
|
||||
const execute = vi.fn(async () => [{ xid: "42", status: "committed" }]);
|
||||
const owner = installDatabaseWorkSignals({ execute, async transaction(callback: (tx: unknown) => Promise<unknown>) {
|
||||
const owner = installDatabaseWorkSignals({ execute, async transaction(callback: (tx: unknown) => Promise<unknown>): Promise<unknown> {
|
||||
return callback({ execute, transaction: owner.transaction });
|
||||
} });
|
||||
const onError = vi.fn();
|
||||
|
||||
@@ -83,8 +83,8 @@ describePostgres("bundled image registration in PostgreSQL", () => {
|
||||
const [companyA, companyB, unconfigured] = await db.insert(companies).values([
|
||||
{ name: "Company A", issuePrefix: "CFA" }, { name: "Company B", issuePrefix: "CFB" }, { name: "Unconfigured", issuePrefix: "CFC" },
|
||||
]).returning();
|
||||
await registry.upsertConfig(row.id, companyA!.id, { configJson: { token: "only-A" } });
|
||||
await registry.upsertConfig(row.id, companyB!.id, { configJson: { token: "only-B" } });
|
||||
await registry.upsertConfig(row.id, companyA!.id, { companyId: companyA!.id, configJson: { token: "only-A" } });
|
||||
await registry.upsertConfig(row.id, companyB!.id, { companyId: companyB!.id, configJson: { token: "only-B" } });
|
||||
row = (await registry.getById(row.id))!;
|
||||
rmSync(old.root, { recursive: true });
|
||||
const original = { ...row };
|
||||
|
||||
@@ -47,7 +47,7 @@ describe("current image plugin runtime loader", () => {
|
||||
fs.writeFileSync(service, (await transform(source, { loader: "ts", format: "esm", target: "node24" })).code);
|
||||
const entry = path.join(root, "server", "dist", "probe.js");
|
||||
fs.writeFileSync(entry, 'import { resolveBundledPluginRuntimeLoader } from "./services/bundled-plugin-runtime.js"; process.stdout.write(resolveBundledPluginRuntimeLoader());');
|
||||
const bundled = await bundleRuntime({ entry, outdir: path.join(root, "server", "dist-runtime") });
|
||||
const bundled = await bundleRuntime({ entry, outdir: path.join(root, "server", "dist-runtime"), maintenanceEntry: undefined, ephemeralMigrationEntry: undefined });
|
||||
expect(execFileSync(process.execPath, [bundled.outfile], { encoding: "utf8" })).toBe(loader);
|
||||
});
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ function makeDeps(overrides?: {
|
||||
const installedRows = new Map(
|
||||
Object.entries(rows).map(([key, row]) => [key, normalize(row)] as const),
|
||||
);
|
||||
const installPlugin = vi.fn(async ({ localPath }: { localPath: string }) => {
|
||||
const installPlugin = vi.fn(async ({ localPath }: { localPath: string }): Promise<{ manifest: { id: string } | null }> => {
|
||||
if (overrides?.installError) throw overrides.installError;
|
||||
const entry = BUNDLED_PLUGIN_CATALOG.find((candidate) =>
|
||||
localPath.endsWith(candidate.relativePath),
|
||||
|
||||
@@ -76,6 +76,7 @@ describeEmbeddedPostgres("cases routes", () => {
|
||||
const storage: StorageService = {
|
||||
provider: "local_disk",
|
||||
async putFile(input) {
|
||||
if (!Buffer.isBuffer(input.body)) throw new Error("Expected bounded bytes");
|
||||
return {
|
||||
provider: "local_disk",
|
||||
objectKey: `${input.namespace}/${randomUUID()}`,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -517,9 +517,9 @@ describeEmbeddedPostgres(
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ kind: "suggest_tasks", payload: { version: 1, tasks: [{ clientKey: "docs", title: "Review the migration", description: "Verify preserved configuration" }] }, expected: "Review the migration" },
|
||||
{ kind: "request_item_verdicts", payload: { version: 1, prompt: "Review each change", items: [{ id: "docs", label: "Migration notes", description: "Verify preserved configuration" }] }, expected: "Migration notes" },
|
||||
] as const)("delivers and settles $kind reviews in Slack", async ({ kind, payload, expected }) => {
|
||||
{ kind: "suggest_tasks" as const, payload: { version: 1 as const, tasks: [{ clientKey: "docs", title: "Review the migration", description: "Verify preserved configuration" }] }, expected: "Review the migration" },
|
||||
{ kind: "request_item_verdicts" as const, payload: { version: 1 as const, prompt: "Review each change", items: [{ id: "docs", label: "Migration notes", description: "Verify preserved configuration" }] }, expected: "Migration notes" },
|
||||
])("delivers and settles $kind reviews in Slack", async ({ kind, payload, expected }) => {
|
||||
const fixture = await seedBoundIssue();
|
||||
const service = issueThreadInteractionService(db);
|
||||
const issue = { id: fixture.issueId, companyId: fixture.companyId };
|
||||
|
||||
@@ -330,7 +330,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
await db.insert(chatPublications).values(ids.map(id => ({
|
||||
companyId: f.companyId, endpointId: f.endpointId, conversationId: f.conversationId,
|
||||
issueId: f.issueId, idempotencyKey: `run:${id}:working:${f.endpointId}`,
|
||||
payload: { text: "Maya is working…", progressState: "working" }, state: "published",
|
||||
payload: { text: "Maya is working…", progressState: "working" as const }, state: "published" as const,
|
||||
createdAt: new Date(Date.now() - 60_000),
|
||||
})));
|
||||
await db.insert(heartbeatRunEvents).values(ids.map(id => ({
|
||||
|
||||
@@ -300,7 +300,7 @@ import { recoveryPostgresSupport as support } from "./helpers/recovery-postgres.
|
||||
const token = createLocalAgentJwt(f.agentId, f.companyId, "paperclip_runner", f.runId, f.responsibleUserId)!;
|
||||
const before = await server.db.select({ id: issues.id }).from(issues).where(eq(issues.companyId, f.companyId));
|
||||
const paths = [`/api/companies/${f.companyId}/issues`, `/api/issues/${f.issueId}/children`];
|
||||
const selections = [{ projectWorkspaceId: f.projectWorkspaceId }, { executionWorkspaceId: executionWorkspace.id },
|
||||
const selections: Array<Record<string, string>> = [{ projectWorkspaceId: f.projectWorkspaceId }, { executionWorkspaceId: executionWorkspace.id },
|
||||
{ inheritExecutionWorkspaceFromIssueId: f.blockerId }];
|
||||
const create = async (path: string, selection: Record<string, string>) => {
|
||||
const response = await fetch(`${server.apiUrl}${path}`, {
|
||||
|
||||
@@ -37,7 +37,7 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
{ id: closedIssueId, companyId, title: "Closed" },
|
||||
{ id: foreignIssueId, companyId: foreignCompanyId, title: "Foreign" },
|
||||
]);
|
||||
await db.insert(toolApplications).values({ id: applicationId, companyId, name: "Slack", type: "native" });
|
||||
await db.insert(toolApplications).values({ id: applicationId, companyId, name: "Slack", type: "chat" });
|
||||
await db.insert(toolConnections).values({ id: connectionId, companyId, applicationId, name: "Slack", uid: connectionId, transport: "chat_sdk", connectionPurpose: "channel" });
|
||||
await db.insert(chatEndpoints).values({ id: endpointId, companyId, connectionId, provider: "slack", publicId: endpointId, assignedAgentId: agentId });
|
||||
await db.insert(chatConversations).values([
|
||||
@@ -74,7 +74,7 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
(id, company_id, agent_id, status, scope_kind, issue_id, native_issue_id, context_snapshot, updated_at)
|
||||
values ($1, $2, $3, 'running', $4, $5, $6, $7::jsonb, $8)`,
|
||||
[runId, companyId, agentId, "runIssueId" in item ? "issue" : "company",
|
||||
"runIssueId" in item ? item.runIssueId : null, "nativeIssueId" in item ? item.nativeIssueId : null,
|
||||
"runIssueId" in item ? item.runIssueId ?? null : null, "nativeIssueId" in item ? item.nativeIssueId ?? null : null,
|
||||
JSON.stringify(item.context), (item.updatedAt ?? since).toISOString()]);
|
||||
if (item.matches) expectedIds.push(runId);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { createHeartbeatQueueTestDependencies } from "./helpers/heartbeat-queue.
|
||||
import { CHAT_CONTROL_RECOVERY_ADMISSION_KEY, CHAT_CONTROL_RECOVERY_UNRESOLVED_CODE } from "../services/chat-control-recovery-stop.js";
|
||||
import { heartbeatService } from "../services/heartbeat.js";
|
||||
import { commitNativeStatusDecision, dispatchPendingNativeStatusEffects } from "../services/native-runtime/status-decision-committer.js";
|
||||
import { NATIVE_STATUS_ARBITER_POLICY_VERSION, type NativeStatusDecision } from "../services/native-runtime/status-arbiter.js";
|
||||
import { claimQueuedNativeReviewRun } from "../services/native-runtime/native-review-dispatch.js";
|
||||
import { createChatPublicationResolution } from "../services/chat-channels/publications/resolution.js";
|
||||
import { createChatPublicationWorker } from "../services/chat-channels/publications/worker.js";
|
||||
@@ -58,9 +59,12 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
afterAll(async () => { await db?.$client.end({ timeout: 1 }); await temporary?.cleanup(); });
|
||||
function chatContextSnapshot(scope = f) {
|
||||
return { issueId: scope.issueId, source: "chat:slack", endpointId: scope.endpointId, wakeCommentId: scope.commentId };
|
||||
}
|
||||
function runValues(scope = f, patch: Partial<typeof heartbeatRuns.$inferInsert> = {}) {
|
||||
return { id: randomUUID(), companyId: scope.companyId, agentId: scope.agentId, responsibleUserId: scope.userId, status: "running", updatedAt: new Date(),
|
||||
contextSnapshot: { issueId: scope.issueId, source: "chat:slack", endpointId: scope.endpointId, wakeCommentId: scope.commentId }, ...patch };
|
||||
contextSnapshot: chatContextSnapshot(scope), ...patch };
|
||||
}
|
||||
async function run(scope = f, patch: Partial<typeof heartbeatRuns.$inferInsert> = {}) {
|
||||
const values = runValues(scope, patch);
|
||||
@@ -155,7 +159,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
let admittedId: string | undefined;
|
||||
await adapter.withIssueExecutionLock({ companyId: f.companyId, runId: finishing.id, now: new Date() }, async (locked, ports) => {
|
||||
const input = { companyId: f.companyId, issue: locked.primaryIssue, finishingRun: locked.run,
|
||||
contextSnapshot: runValues().contextSnapshot, responsibleUserId: f.userId, sessionBefore: null, now: new Date(),
|
||||
contextSnapshot: chatContextSnapshot(), responsibleUserId: f.userId, sessionBefore: null, now: new Date(),
|
||||
};
|
||||
const agent = { id: f.agentId, companyId: f.companyId, name: "Maya", invokable: true };
|
||||
if (kind === "deferred") {
|
||||
@@ -186,7 +190,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
dependencies.getSchedulingSuppression.mockResolvedValue({ suppressed: true, reason: "task_drain" });
|
||||
await start(); await flush(); await quiet();
|
||||
const admitted = await service.enqueueWakeup(f.agentId, { source: "on_demand", reason: "issue_commented",
|
||||
contextSnapshot: runValues().contextSnapshot, requestedByActorType: "user", requestedByActorId: f.userId,
|
||||
contextSnapshot: chatContextSnapshot(), requestedByActorType: "user", requestedByActorId: f.userId,
|
||||
});
|
||||
expect(admitted).toMatchObject({ status: "queued", companyId: f.companyId });
|
||||
await flush();
|
||||
@@ -268,7 +272,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
dependencies.getSchedulingSuppression.mockResolvedValue({ suppressed: true, reason: "task_drain" });
|
||||
await start(); await flush(); await quiet();
|
||||
const admitted = await service.enqueueWakeup(replacement.id, { source: "on_demand", reason: "issue_commented",
|
||||
contextSnapshot: runValues().contextSnapshot, requestedByActorType: "user", requestedByActorId: f.userId,
|
||||
contextSnapshot: chatContextSnapshot(), requestedByActorType: "user", requestedByActorId: f.userId,
|
||||
});
|
||||
expect(admitted).toMatchObject({ agentId: replacement.id, status: "queued" });
|
||||
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, old.id)))[0])
|
||||
@@ -326,7 +330,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
await db.transaction(async tx => {
|
||||
await createWakeAdmissionWriter().coalesceIntoActiveExecutionRun(
|
||||
createAdmissionTransactionScope(f.companyId, tx as unknown as typeof db), {
|
||||
companyId: f.companyId, activeExecutionRunId: seeded.id, mergedContextSnapshot: runValues().contextSnapshot,
|
||||
companyId: f.companyId, activeExecutionRunId: seeded.id, mergedContextSnapshot: chatContextSnapshot(),
|
||||
agentId: f.agentId, source: "on_demand", triggerDetail: null, payload: { issueId: f.issueId },
|
||||
requestedByActorType: "user", requestedByActorId: f.userId, idempotencyKey: null,
|
||||
});
|
||||
@@ -411,11 +415,11 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
});
|
||||
return { companyId: f.companyId, issueId: f.issueId, runId: native.row.id, assessmentId,
|
||||
priorStatus: "in_progress", priorStatusVersion: 0, priorDecisionId: null,
|
||||
decision: { policyVersion: "phase6-v1", statusAction: "preserve" as const, toStatus: "in_progress" as const,
|
||||
decision: { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, statusAction: "preserve" as const, toStatus: "in_progress" as const,
|
||||
reasonCode: "live_continuation_registered", unblockDescriptor: null,
|
||||
effects: [{ kind: "enqueue_continuation" as const, agentId: f.agentId, continuationKind: "same_agent" as const,
|
||||
summary: "Continue responding", idempotencyKey: `continuation:${native.row.id}` }],
|
||||
},
|
||||
} satisfies NativeStatusDecision,
|
||||
};
|
||||
}
|
||||
it("wakes from a governed native reviewer claim and leaves repeated claims quiet", async () => {
|
||||
@@ -576,7 +580,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
const delivery = questionResponseDeliveryService(db, { heartbeat: {
|
||||
cancelRun: async () => { throw new Error("The source is already terminal"); },
|
||||
wakeup: async (_agentId, options) => {
|
||||
const values = runValues(f, { status: "queued", contextSnapshot: options.contextSnapshot });
|
||||
const values = runValues(f, { status: "queued", contextSnapshot: options?.contextSnapshot });
|
||||
const [target] = await db.insert(heartbeatRuns).values(values).returning();
|
||||
targetId = target.id;
|
||||
await flush();
|
||||
@@ -714,6 +718,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
let beforeReceiptKeys: string[] | undefined;
|
||||
await start(); await flush(); await quiet();
|
||||
const deliver = questionResponseDeliveryService(db, { heartbeat: {
|
||||
cancelRun: async () => { throw new Error("The source is already terminal"); },
|
||||
wakeup: async () => {
|
||||
const [target] = await db.insert(heartbeatRuns).values(runValues(f, {
|
||||
id: targetId, status: "queued", contextSnapshot: { issueId: f.issueId },
|
||||
@@ -731,7 +736,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
});
|
||||
// Controlled transport/authorization ports; actual attempt lease, receipt
|
||||
// transaction, question delivery receipt and milestone worker use PostgreSQL.
|
||||
const worker = createChatPublicationWorker(db, {
|
||||
const worker = createChatPublicationWorker(db, ({
|
||||
CAPABILITIES, settleGitHubAutomaticPublication: async () => false,
|
||||
ensureDurablePublicationTransport: async publication => publication,
|
||||
tryProcessTeamsFilePublication: async () => false,
|
||||
@@ -749,7 +754,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
postSafePublication: post, commitTaskControlCompletion: async () => {}, receiptReactionCompletionRunId: async () => null,
|
||||
finalizePublicationFailure: async (_publication, error) => { errors.push(error); return false; },
|
||||
redactError: String,
|
||||
} as Parameters<typeof createChatPublicationWorker>[1]);
|
||||
} satisfies Partial<Parameters<typeof createChatPublicationWorker>[1]>) as unknown as Parameters<typeof createChatPublicationWorker>[1]);
|
||||
await worker.processSelectedPublication(prompt);
|
||||
expect(post).toHaveBeenCalledTimes(1);
|
||||
expect((await db.select().from(chatPublications).where(eq(chatPublications.id, prompt.id)))[0]).toMatchObject({ state: "published", providerMessageId: "published-question" });
|
||||
@@ -794,7 +799,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
await quiet();
|
||||
if (resolution) {
|
||||
const targetId = randomUUID();
|
||||
await questionResponseDeliveryService(db, { heartbeat: { wakeup: async () => {
|
||||
await questionResponseDeliveryService(db, { heartbeat: { cancelRun: async () => { throw new Error("The source is already terminal"); }, wakeup: async () => {
|
||||
const [target] = await db.insert(heartbeatRuns).values(runValues(f, {
|
||||
id: targetId, status: "queued", contextSnapshot: { issueId: f.issueId },
|
||||
})).returning();
|
||||
@@ -804,11 +809,11 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
expect((await publications()).some(row => row.idempotencyKey.startsWith(`run:${targetId}:`))).toBe(false);
|
||||
await quiet();
|
||||
const [endpoint] = await db.select().from(chatEndpoints).where(eq(chatEndpoints.id, f.endpointId));
|
||||
const resolver = createChatPublicationResolution(db, {
|
||||
const resolver = createChatPublicationResolution(db, ({
|
||||
endpointRecord: async () => ({ endpoint, assignedAgentName: "Maya", connectionName: "Chat", applicationId: "test", credentialSecretRefs: [] }),
|
||||
withCredentialMutationLease: async (_endpoint, operation) => operation({ assertOwned: async () => {} }),
|
||||
commitTaskControlCompletion: async () => {}, processPendingPublications: async () => {},
|
||||
} as Parameters<typeof createChatPublicationResolution>[1]);
|
||||
commitTaskControlCompletion: async () => {}, processPendingPublications: async () => 0,
|
||||
} satisfies Partial<Parameters<typeof createChatPublicationResolution>[1]>) as unknown as Parameters<typeof createChatPublicationResolution>[1]);
|
||||
await resolver.resolvePublication(f.endpointId, prompt.id, resolution, f.userId);
|
||||
await flush();
|
||||
const keys = (await publications()).map(row => row.idempotencyKey);
|
||||
@@ -928,7 +933,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
await start(); await flush(); expect(await publications()).toEqual([]); await quiet();
|
||||
await db.transaction(async tx => {
|
||||
await notifyChatRunMilestones(tx, f);
|
||||
await tx.update(heartbeatRuns).set({ contextSnapshot: runValues().contextSnapshot }).where(eq(heartbeatRuns.id, row.id));
|
||||
await tx.update(heartbeatRuns).set({ contextSnapshot: chatContextSnapshot() }).where(eq(heartbeatRuns.id, row.id));
|
||||
});
|
||||
await flush(); expect(await publications()).toHaveLength(1); await quiet();
|
||||
});
|
||||
|
||||
@@ -177,6 +177,7 @@ type CapturePayload = {
|
||||
skillEntries: string[];
|
||||
claudeConfigDir: string | null;
|
||||
claudeConfigEntries?: string[];
|
||||
mcpConfigContents?: string | null;
|
||||
paperclipApiUrl?: string | null;
|
||||
paperclipApiKey?: string | null;
|
||||
paperclipApiBridgeMode?: string | null;
|
||||
@@ -1058,7 +1059,7 @@ describe("claude execute", () => {
|
||||
const preparedCommand = path.join(remoteWorkspace, ".paperclip-runtime/claude-cli/2.1.280/bin/claude");
|
||||
const logs: string[] = [];
|
||||
const commands: string[] = [];
|
||||
const loggedCommands: string[] = [];
|
||||
const loggedCommands: Array<string | undefined> = [];
|
||||
try {
|
||||
await fs.writeFile(commandPath, (await fs.readFile(commandPath, "utf8")).replace("2.1.286", "2.1.263"));
|
||||
await fs.mkdir(remoteWorkspace, { recursive: true });
|
||||
@@ -1081,7 +1082,7 @@ describe("claude execute", () => {
|
||||
return runner.execute(input);
|
||||
} } },
|
||||
authToken: "run-jwt-token", onLog: async (_stream, chunk) => { logs.push(chunk); },
|
||||
onMeta: async meta => { commands.push(meta.command); loggedCommands.push(meta.env.PAPERCLIP_RESOLVED_COMMAND); },
|
||||
onMeta: async meta => { commands.push(meta.command); loggedCommands.push(meta.env?.PAPERCLIP_RESOLVED_COMMAND); },
|
||||
});
|
||||
if (state === "compatible") {
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("unclaimed Cloud background work", () => {
|
||||
const isEnabled = vi.fn().mockResolvedValue(false);
|
||||
const db = installDatabaseWorkSignals({ execute: vi.fn(), transaction: vi.fn() }) as unknown as Db;
|
||||
const coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => !standby, onError: vi.fn() });
|
||||
const events = createPublicMcpEvents(db, { isEnabled } as PublicMcpOAuth, vi.fn(), {
|
||||
const events = createPublicMcpEvents(db, { isEnabled } as unknown as PublicMcpOAuth, vi.fn(), {
|
||||
isBackgroundWorkEnabled: () => !standby,
|
||||
});
|
||||
try {
|
||||
|
||||
@@ -1339,7 +1339,7 @@ process.exit(1);
|
||||
|
||||
const policy = conversationMode
|
||||
? buildPaperclipTaskMarkdown({
|
||||
issue: { id: "issue-1", title: "Chat", workMode: "planning", conversationAgentId: "agent-1" },
|
||||
issue: { id: "issue-1", identifier: null, title: "Chat", workMode: "planning", conversationAgentId: "agent-1" },
|
||||
interaction: { kind: "request_confirmation", status: "rejected" },
|
||||
planReview: { status: "rejected", reason: "Revise the final note." },
|
||||
includeDescription: false,
|
||||
@@ -1437,7 +1437,7 @@ process.exit(1);
|
||||
expect(invocationPrompt).toContain(policy);
|
||||
if (resumedSession) expect(capture.prompt).not.toContain("Full description that must not replay");
|
||||
else expect(capture.prompt).toContain("Full description that must not replay");
|
||||
expect(promptMetrics.taskContextChars).toBe(resumedSession ? policy.length : `Full description that must not replay\n${policy}`.length);
|
||||
expect(promptMetrics.taskContextChars).toBe(resumedSession ? policy!.length : `Full description that must not replay\n${policy}`.length);
|
||||
if (conversationMode) {
|
||||
expect(invocationPrompt).toContain(AGENT_CHAT_DIRECTIVE);
|
||||
expect(invocationPrompt).toContain("baseRevisionId set to that latestRevisionId");
|
||||
|
||||
@@ -60,10 +60,12 @@ describeEmbeddedPostgres("companyService", () => {
|
||||
lifecycleWorker = startAgentLifecycle(db, { ...createLifecycleDriver(db, {} as never),
|
||||
requiredPluginIds: async () => [], runPlugin: async () => "complete" }, () => true);
|
||||
});
|
||||
const lifecycleStateFor = (status: typeof agents.$inferInsert["status"]): NonNullable<typeof agents.$inferInsert["lifecycleState"]> =>
|
||||
status === "paused" || status === "pending_approval" || status === "terminated" ? status : "ready";
|
||||
async function insertAgents(input: typeof agents.$inferInsert | Array<typeof agents.$inferInsert>) {
|
||||
const rows = Array.isArray(input) ? input : [input];
|
||||
await db.insert(agents).values(rows.map(row => ({ ...row,
|
||||
lifecycleState: ["paused", "pending_approval", "terminated"].includes(row.status ?? "") ? row.status! : "ready",
|
||||
lifecycleState: lifecycleStateFor(row.status),
|
||||
lifecycleHolds: row.status === "paused" ? [row.pauseReason ?? "manual"] : [],
|
||||
})));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const companySvc = {
|
||||
// Async-empty default (not a bare vi.fn()): every new-company import reads
|
||||
// the existing names for de-duplication, including describes that never
|
||||
// touch this mock.
|
||||
list: vi.fn(async () => []),
|
||||
list: vi.fn(async (): Promise<Array<{ name: string }>> => []),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
};
|
||||
@@ -516,7 +516,7 @@ describe("company portability", () => {
|
||||
entryFile: "AGENTS.md",
|
||||
warnings: [],
|
||||
}));
|
||||
agentInstructionsSvc.materializeManagedBundle.mockImplementation(async (agent: { adapterConfig: Record<string, unknown> }) => ({
|
||||
agentInstructionsSvc.materializeManagedBundle.mockImplementation(async (agent: { id: string; adapterConfig: Record<string, unknown> }) => ({
|
||||
bundle: null,
|
||||
adapterConfig: {
|
||||
...agent.adapterConfig,
|
||||
|
||||
@@ -5,6 +5,10 @@ import { issueRoutes } from "../routes/issues.js";
|
||||
import { createCompanySearchRateLimiter } from "../services/company-search-rate-limit.js";
|
||||
import type { CompanySearchQuery, CompanySearchResponse } from "@paperclipai/shared";
|
||||
|
||||
async function unusedExtract(): Promise<never> {
|
||||
throw new Error("Rate limit tests do not call extract");
|
||||
}
|
||||
|
||||
function createSearchResponse(query: CompanySearchQuery): CompanySearchResponse {
|
||||
return {
|
||||
query: query.q,
|
||||
@@ -44,7 +48,7 @@ describe("company search route rate limiting", () => {
|
||||
next();
|
||||
});
|
||||
app.use("/api", issueRoutes({} as never, {} as never, {
|
||||
searchService: { search },
|
||||
searchService: { search, extract: unusedExtract },
|
||||
searchRateLimiter: createCompanySearchRateLimiter({
|
||||
maxRequests: 1,
|
||||
windowMs: 60_000,
|
||||
@@ -76,7 +80,7 @@ describe("company search route rate limiting", () => {
|
||||
next();
|
||||
});
|
||||
app.use("/api", issueRoutes({} as never, {} as never, {
|
||||
searchService: { search },
|
||||
searchService: { search, extract: unusedExtract },
|
||||
searchRateLimiter: createCompanySearchRateLimiter({
|
||||
maxRequests: 10,
|
||||
windowMs: 60_000,
|
||||
@@ -104,7 +108,7 @@ describe("company search route rate limiting", () => {
|
||||
next();
|
||||
});
|
||||
app.use("/api", issueRoutes({} as never, {} as never, {
|
||||
searchService: { search },
|
||||
searchService: { search, extract: unusedExtract },
|
||||
searchRateLimiter: createCompanySearchRateLimiter({
|
||||
maxRequests: 10,
|
||||
windowMs: 60_000,
|
||||
|
||||
@@ -80,7 +80,6 @@ describeEmbeddedPostgres("company skill policy routes", () => {
|
||||
source: "agent_key",
|
||||
agentId,
|
||||
companyId: actor === "cross-company" ? otherCompanyId : companyId,
|
||||
runId: null,
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { requestHumanInputAction } from "../../../packages/paperclip-runner/src/protocol-actions/request-human-input.js";
|
||||
import { questionSetToAskUserQuestionsPayload } from "@paperclipai/shared";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
@@ -240,7 +240,7 @@ async function createRemoteMcpToolFixture(db: ReturnType<typeof createDb>, compa
|
||||
|
||||
describe("Honcho workspace configuration", () => {
|
||||
const schema = { type: "object", properties: { workspace_id: { type: "string" }, query: { type: "string" } }, required: ["workspace_id", "query"] };
|
||||
const connection = { config: { sourceTemplateKey: "honcho", connectionMethodKey: "mcp-api-key", methodConfig: { workspaceId: "team-workspace" } } } as typeof toolConnections.$inferSelect;
|
||||
const connection = { transport: "mcp_remote" as const, config: { sourceTemplateKey: "honcho", connectionMethodKey: "mcp-api-key", methodConfig: { workspaceId: "team-workspace" } } };
|
||||
it("projects the workspace and hides its argument while keeping old unconfigured tools usable", () => {
|
||||
expect(projectedConnectionToolArguments(connection, { workspace_id: "forged", query: "decisions" }, "query", schema)).toEqual({ workspace_id: "team-workspace", query: "decisions" });
|
||||
expect(projectedConnectionToolInputSchema(connection, schema, "query")).toEqual({ type: "object", properties: { query: { type: "string" } }, required: ["query"] });
|
||||
|
||||
@@ -980,7 +980,7 @@ describeEmbeddedPostgres("connectionIntentService", () => {
|
||||
const access = toolAccessService(db);
|
||||
const effective = await access.getEffectiveProfilesForAgent(f.company.id, f.agent.id);
|
||||
const profile = effective.profiles.find(profile => profile.profileKey.startsWith("connection-intent:"))!;
|
||||
await access.deleteProfile(profile.id, {});
|
||||
await access.deleteProfile(profile.id, { force: false });
|
||||
expect((await access.getEffectiveProfilesForAgent(f.company.id, f.agent.id)).allowedTools).toEqual([]);
|
||||
for (const tool of f.catalog.slice(0, 2)) {
|
||||
const decision = await toolAccessPolicyService(db).decide({
|
||||
|
||||
@@ -79,7 +79,7 @@ describe("connection management with production default agents", () => {
|
||||
await server.db.update(heartbeatRuns).set({ agentId: agent.id }).where(eq(heartbeatRuns.id, base.runId));
|
||||
await server.db.update(issues).set({ assigneeAgentId: agent.id, responsibleUserId: userId }).where(eq(issues.id, base.issueId));
|
||||
const key = await agentService(server.db).createApiKey(agent.id, "integration", { kind: "standard" }, { responsibleUserId: userId });
|
||||
const jwt = createLocalAgentJwt(agent.id, base.companyId, "paperclip_runner", base.runId, userId);
|
||||
const jwt = createLocalAgentJwt(agent.id, base.companyId, "paperclip_runner", base.runId, userId)!;
|
||||
const request = async (method: string, path: string, body?: unknown, token = key.token) => {
|
||||
const response = await fetch(server.apiUrl + (path.startsWith("/mcp/") ? "" : "/api") + path, { method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
||||
return { status: response.status, body: await response.json().catch(() => null) };
|
||||
@@ -480,7 +480,7 @@ describe("connection management with production default agents", () => {
|
||||
try {
|
||||
const [environment] = await server.db.insert(environments).values({ name: `Login fixture ${f.agentId}`, driver: "sandbox" }).returning();
|
||||
const [session] = await server.db.insert(adapterAuthSessions).values({ companyId: f.companyId, environmentId: environment.id, adapterType: "codex_local", startedByUserId: f.userId,
|
||||
publicSessionId: randomUUID(), status: "waiting_for_user", expiresAt: new Date(Date.now() + 60_000), aiConnection: { ownership: "personal", name: "Saved login", allAgents: false, agentIds: [] } }).returning();
|
||||
publicSessionId: randomUUID(), status: "waiting_for_user", expiresAt: new Date(Date.now() + 60_000), aiConnection: { provider: "openai", method: "subscription", ownership: "personal", name: "Saved login", allAgents: false, agentIds: [] } }).returning();
|
||||
const path = `/companies/${f.companyId}/adapters/codex_local/login-sessions`;
|
||||
expect(await f.request("GET", `${path}/active`)).toMatchObject({ status: 200, body: { sessionId: session.publicSessionId } });
|
||||
expect(await f.request("GET", `${path}/${session.publicSessionId}`, undefined, f.jwt)).toMatchObject({ status: 200, body: { sessionId: session.publicSessionId } });
|
||||
@@ -569,7 +569,7 @@ describe("connection management with production default agents", () => {
|
||||
}
|
||||
if (revocation === "other-agent-credential" || revocation === "other-agent-credential-none") {
|
||||
await server.db.update(toolConnections).set({ authKind: revocation === "other-agent-credential-none" ? "none" : "api_key", credentialPolicy: "per_agent" }).where(eq(toolConnections.id, created.body.id));
|
||||
await server.db.insert(connectionGrants).values([f.agentId, targetAgentId].map(subjectAgentId => ({ companyId: f.companyId, connectionId: created.body.id, kind: "agent" as const, subjectAgentId, status: "active" })));
|
||||
await server.db.insert(connectionGrants).values([f.agentId, targetAgentId].map(subjectAgentId => ({ companyId: f.companyId, connectionId: created.body.id, kind: "agent" as const, subjectAgentId, status: "active" as const })));
|
||||
expect(await f.request("POST", path, body, f.jwt)).toMatchObject({ status: 403, body: { reasonCode: "test_credential_identity_mismatch" } });
|
||||
expect(calls).toBe(0);
|
||||
return;
|
||||
@@ -688,7 +688,7 @@ describe("connection management with production default agents", () => {
|
||||
expect(settled.policyExplanation?.delegatedCallerConsumedRateAdmissions).toHaveLength(1);
|
||||
const [nextRun] = await server.db.insert(heartbeatRuns).values({ companyId: f.companyId, agentId: f.agentId,
|
||||
status: "running", responsibleUserId: f.userId, contextSnapshot: { issueId: f.issueId } }).returning();
|
||||
const nextJwt = createLocalAgentJwt(f.agentId, f.companyId, "paperclip_runner", nextRun.id, f.userId);
|
||||
const nextJwt = createLocalAgentJwt(f.agentId, f.companyId, "paperclip_runner", nextRun.id, f.userId)!;
|
||||
expect(await f.request("POST", path, body, nextJwt)).toMatchObject({ status: 200, body: { decision: "off", error: { reasonCode: "rate_limited" } } });
|
||||
expect(calls).toBe(1);
|
||||
}
|
||||
@@ -732,7 +732,7 @@ describe("connection management with production default agents", () => {
|
||||
const ownBinding = await f.request("POST", `${path}/bind`, { targetType: "issue", targetId: f.issueId });
|
||||
expect(ownBinding, JSON.stringify(ownBinding)).toMatchObject({ status: 201 });
|
||||
expect(await f.request("POST", `${path}/unbind`, { targetType: "issue", targetId: f.issueId })).toMatchObject({ status: 200 });
|
||||
for (const target of [{ targetType: "project", targetId: f.projectId }, { targetType: "issue", targetId: issue.id }, { targetType: "routine", targetId: routine.id }, { targetType: "gateway", targetId: gateway.body.id }]) {
|
||||
for (const target of [{ targetType: "project" as const, targetId: f.projectId }, { targetType: "issue" as const, targetId: issue.id }, { targetType: "routine" as const, targetId: routine.id }, { targetType: "gateway" as const, targetId: gateway.body.id }]) {
|
||||
expect(await f.request("POST", `${path}/bind`, target), JSON.stringify(target)).toMatchObject({ status: 403 });
|
||||
const [saved] = await server.db.insert(toolProfileBindings).values({ ...target, companyId: f.companyId, profileId: profile.body.id }).returning();
|
||||
expect(await f.request("PATCH", `/tool-profiles/${profile.body.id}`, { description: "Cannot edit retained target" })).toMatchObject({ status: 403 });
|
||||
@@ -786,7 +786,7 @@ describe("connection management with production default agents", () => {
|
||||
return originalFrom(table);
|
||||
}) as typeof builder.from;
|
||||
return builder;
|
||||
}) as typeof server.db.select);
|
||||
}) as unknown as typeof server.db.select);
|
||||
try {
|
||||
const path = `/companies/${f.companyId}/tools/policies`;
|
||||
const body = { name: "Bounded policy", policyType: "allow", selectors: { agentId: f.agentId, connectionIds: Array(5000).fill(created.body.id) } };
|
||||
@@ -1414,7 +1414,7 @@ describe("connection management with production default agents", () => {
|
||||
externalConversationId: randomUUID(), externalLabel: "History" }))).returning();
|
||||
const rows = Array.from({ length: 30 }, (_, index) => ({ id: `${f.agentId.slice(0, 24)}${index.toString(16).padStart(12, "0")}`,
|
||||
companyId: f.companyId, endpointId: endpoint.body.id, conversationId: conversations[index < 3 ? 0 : 1].id,
|
||||
providerEventId: randomUUID(), deduplicationKey: randomUUID(), eventKind: index < 3 ? "readable_history" : "private_history", state: "processed", normalizedEvent: {}, createdAt: new Date("2026-10-25T12:00:00.000Z") }));
|
||||
providerEventId: randomUUID(), deduplicationKey: randomUUID(), eventKind: "message" as const, state: "processed" as const, redactedError: index < 3 ? null : "private_history", normalizedEvent: {}, createdAt: new Date("2026-10-25T12:00:00.000Z") }));
|
||||
await server.db.insert(chatDeliveries).values(rows);
|
||||
const path = `/chat-endpoints/${endpoint.body.id}/activity?limit=25`;
|
||||
const first = await f.request("GET", path, undefined, f.jwt);
|
||||
@@ -1594,7 +1594,7 @@ describe("connection management with production default agents", () => {
|
||||
if (mode === "ask-rate") await server.db.insert(toolPolicies).values({ companyId: f.companyId, name: "One issuer admission", policyType: "rate_limit", selectors: { connectionId: connection.body.id, agentId: f.agentId }, priority: -1, config: { limit: 1, windowSeconds: 60, keyBy: ["agent", "tool"] } });
|
||||
if (mode === "credential") {
|
||||
await server.db.update(toolConnections).set({ credentialPolicy: "per_agent" }).where(eq(toolConnections.id, connection.body.id));
|
||||
await server.db.insert(connectionGrants).values([f.agentId, target.id].map(subjectAgentId => ({ companyId: f.companyId, connectionId: connection.body.id, kind: "agent" as const, subjectAgentId, status: "active" })));
|
||||
await server.db.insert(connectionGrants).values([f.agentId, target.id].map(subjectAgentId => ({ companyId: f.companyId, connectionId: connection.body.id, kind: "agent" as const, subjectAgentId, status: "active" as const })));
|
||||
}
|
||||
const called = await rpc("tools/call", { name: tool.name, arguments: {} });
|
||||
if (isRate) {
|
||||
@@ -1896,7 +1896,7 @@ describe("connection management with production default agents", () => {
|
||||
await server.db.update(agentApiKeys).set({ responsibleUserId: null }).where(eq(agentApiKeys.id, f.key.id));
|
||||
expect((await f.request("GET", `/companies/${f.companyId}/tools/gallery`)).status).toBeGreaterThanOrEqual(400);
|
||||
const g = await fixture();
|
||||
await server.db.update(agentApiKeys).set({ scopeConfig: { kind: "task_bridge", issueId: g.issueId } }).where(eq(agentApiKeys.id, g.key.id));
|
||||
await server.db.update(agentApiKeys).set({ scopeConfig: { kind: "task_bridge", parentIssueId: g.issueId } }).where(eq(agentApiKeys.id, g.key.id));
|
||||
expect((await g.request("GET", `/companies/${g.companyId}/tools/gallery`)).status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
|
||||
|
||||
@@ -104,7 +104,8 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
remoteHttpEndpointLookup: async () => [{ address: "8.8.8.8", family: 4 }] });
|
||||
const actor = { actorType: "user" as const, actorId: "policy-reviewer" };
|
||||
const connection = await service.createConnection(company.id, {
|
||||
name: "Custom reviewed endpoint", transport: "mcp_remote", enabled: true, status: "active",
|
||||
name: "Custom reviewed endpoint", connectionPurpose: "tool", transport: "mcp_remote", authKind: "none", ownership: "customer", connectionKind: "managed",
|
||||
transportConfig: {}, credentialSecretRefs: [], enabled: true, status: "active",
|
||||
config: { sourceTemplateKey: "custom", url: RAILWAY_MCP_URL, quarantineNewEntries: true },
|
||||
}, actor);
|
||||
const initial = await service.refreshCatalog(connection.id, actor, { enableAllByDefault: true });
|
||||
@@ -112,8 +113,8 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
tools = [...tools, { name: "provider_added_tool", annotations: { readOnlyHint: true } }];
|
||||
const refreshed = await service.refreshCatalog(connection.id, actor, { enableAllByDefault: true });
|
||||
expect(refreshed.catalog.find(tool => tool.toolName === "provider_added_tool")).toMatchObject({ riskLevel: "write", status: "quarantined" });
|
||||
expect(refreshed.connection.config.quarantineNewEntries).toBe(true);
|
||||
expect(refreshed.connection.config.sourceTemplateKey).toBe("custom");
|
||||
expect(refreshed.connection.config?.quarantineNewEntries).toBe(true);
|
||||
expect(refreshed.connection.config?.sourceTemplateKey).toBe("custom");
|
||||
expect(request).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,7 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
if (reply === "response") await db.insert(issueComments).values({ companyId, issueId,
|
||||
createdByRunId: runId, authorAgentId: agentId, origin: "comment", body: padding });
|
||||
if (reply === "interaction") await db.insert(issueThreadInteractions).values({ companyId, issueId,
|
||||
sourceRunId: runId, createdByAgentId: agentId, status: "pending", kind: "request_confirmation", payload: { version: 1 } });
|
||||
sourceRunId: runId, createdByAgentId: agentId, status: "pending", kind: "request_confirmation", payload: { version: 1, prompt: "Approve?" } });
|
||||
return { companyId, agentId, issueId, runId, wakeId, wakeTime };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createServer } from "node:http";
|
||||
import { createServer, type RequestListener } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { generateKeyPairSync, randomUUID } from "node:crypto";
|
||||
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
@@ -118,7 +118,7 @@ describe.skipIf(!cloudDist)("customer-success managed agent / Cloud / tenant qua
|
||||
.values({ companyId: customer.id, projectId: project.id, title: "First successful task" })
|
||||
.returning();
|
||||
const storage = { provider: "local_disk" } as StorageService;
|
||||
const start = async (handler: Parameters<typeof createServer>[0]) => {
|
||||
const start = async (handler: RequestListener) => {
|
||||
const server = createServer(handler);
|
||||
servers.push(server);
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
@@ -289,7 +289,7 @@ describe.skipIf(!cloudDist)("customer-success managed agent / Cloud / tenant qua
|
||||
env: { PAPERCLIP_CUSTOMER_SUCCESS_CLOUD_ORIGIN: cloudOrigin },
|
||||
},
|
||||
context: {},
|
||||
runtime: { sessionId: null, sessionParams: null, taskKey: null },
|
||||
runtime: { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null },
|
||||
onLog: async (_stream, chunk) => {
|
||||
output.push(chunk);
|
||||
},
|
||||
|
||||
@@ -559,7 +559,7 @@ describe("customer-success read authority", () => {
|
||||
companyId,
|
||||
companySkillId: skill.id,
|
||||
revisionNumber: 1,
|
||||
fileInventory: [{ path: "SKILL.md", kind: "markdown", sizeBytes: 9, content: "# Example" }],
|
||||
fileInventory: [{ path: "SKILL.md", kind: "markdown", content: "# Example" }],
|
||||
})
|
||||
.returning();
|
||||
await db
|
||||
|
||||
@@ -49,7 +49,7 @@ const credential = "daytona-fixture-secret";
|
||||
throw new Error("Unexpected provider request");
|
||||
});
|
||||
const service = toolAccessService(db, { remoteHttpRequest: request, remoteHttpEndpointLookup: async () => [{ address: "8.8.8.8", family: 4 }] });
|
||||
const connected = await service.connectGalleryApp(company.id, { galleryKey: "daytona", methodKey: "workspace-diagnostics",
|
||||
const connected = await service.connectGalleryApp(company.id, { galleryKey: "daytona", connectionMethodKey: "workspace-diagnostics",
|
||||
...(personal ? { grantKind: "user" as const } : {}), credentialValues: { "credentials.authorization": credential } }, actor);
|
||||
const [agent] = await db.insert(agents).values({ companyId: company.id, name: "Maintainer", role: "engineer", adapterType: "process" }).returning();
|
||||
await service.finishGalleryAppConnection(company.id, connected.connectionId, { enabledCatalogEntryIds: connected.catalog.map(c => c.id),
|
||||
@@ -156,7 +156,7 @@ const credential = "daytona-fixture-secret";
|
||||
const legacy = await db.insert(toolCatalogEntries).values(legacyNames.map(toolName => ({
|
||||
companyId: f.company.id, applicationId: f.connected.connection.applicationId, connectionId: f.connected.connectionId,
|
||||
name: toolName, toolName, title: toolName === "daytona_get_workspace_recovery" ? "Get workspace recovery" : toolName,
|
||||
entryKind: "tool", status: "active", riskLevel: "read", isReadOnly: true,
|
||||
entryKind: "tool" as const, status: "active" as const, riskLevel: "read" as const, isReadOnly: true,
|
||||
inputSchema: { type: "object" }, versionHash: "legacy", schemaHash: "legacy",
|
||||
}))).returning();
|
||||
await f.service.finishGalleryAppConnection(f.company.id, f.connected.connectionId, {
|
||||
|
||||
@@ -16,7 +16,7 @@ it.each([
|
||||
])("preserves $due continuation with $spacing ms minimum spacing and disarms after settlement", async ({ due, spacing, delay }) => {
|
||||
vi.useFakeTimers();
|
||||
const onError = vi.fn();
|
||||
const owner = installDatabaseWorkSignals({ execute: vi.fn(async () => []) });
|
||||
const owner = installDatabaseWorkSignals({ execute: vi.fn(async () => []), transaction: vi.fn() });
|
||||
coordinator = createDeliveryWorkCoordinator({ owner, canRun: () => true, onError });
|
||||
const now = Date.now();
|
||||
const run = vi.fn(async () => {});
|
||||
@@ -68,7 +68,7 @@ it("releases a settled first-write hold during drain and retains a probe-free re
|
||||
it("retains failed work through drain and preserves a future domain deadline", async () => {
|
||||
vi.useFakeTimers();
|
||||
let draining = false;
|
||||
const owner = installDatabaseWorkSignals({ execute: vi.fn(async () => []) });
|
||||
const owner = installDatabaseWorkSignals({ execute: vi.fn(async () => []), transaction: vi.fn() });
|
||||
const run = vi.fn(async () => { if (run.mock.calls.length === 1) { draining = true; throw new Error("retry debt"); } });
|
||||
const deadline = Date.now() + 600_000;
|
||||
const next = vi.fn(async () => deadline);
|
||||
@@ -87,7 +87,7 @@ it("retains failed work through drain and preserves a future domain deadline", a
|
||||
it("does no domain probes in warm standby and reconstructs after admission opens", async () => {
|
||||
vi.useFakeTimers();
|
||||
let standby = true;
|
||||
const owner = installDatabaseWorkSignals({ execute: vi.fn(async () => []) });
|
||||
const owner = installDatabaseWorkSignals({ execute: vi.fn(async () => []), transaction: vi.fn() });
|
||||
const run = vi.fn(async () => {}), next = vi.fn(async () => null);
|
||||
coordinator = createDeliveryWorkCoordinator({ owner, canRun: () => !standby, canReconcile: () => !standby, onError: vi.fn() });
|
||||
const worker = registerDeadlineWork(coordinator, DELIVERY_QUEUES.agentTimers, { run, next, canRun: () => !standby, retryMs: 30_000 });
|
||||
@@ -126,7 +126,7 @@ it("does not release an ambiguous database intent while dispatch is drained", as
|
||||
it("backs off an expired deadline when only the domain is drained and resumes promptly", async () => {
|
||||
vi.useFakeTimers();
|
||||
let paused = false;
|
||||
const owner = installDatabaseWorkSignals({ execute: vi.fn(async () => []) });
|
||||
const owner = installDatabaseWorkSignals({ execute: vi.fn(async () => []), transaction: vi.fn() });
|
||||
const run = vi.fn(async () => {});
|
||||
const domainCanRun = vi.fn(() => !paused);
|
||||
const deadline = Date.now() + 1_000;
|
||||
@@ -155,7 +155,7 @@ it("backs off an expired deadline when only the domain is drained and resumes pr
|
||||
it("retains a prompt paused wake when a new request preempts a far-future deadline", async () => {
|
||||
vi.useFakeTimers();
|
||||
let paused = false;
|
||||
const owner = installDatabaseWorkSignals({ execute: vi.fn(async () => []) });
|
||||
const owner = installDatabaseWorkSignals({ execute: vi.fn(async () => []), transaction: vi.fn() });
|
||||
const run = vi.fn(async () => {});
|
||||
const deadline = Date.now() + 86_400_000;
|
||||
coordinator = createDeliveryWorkCoordinator({ owner, canRun: () => true, onError: vi.fn() });
|
||||
|
||||
@@ -87,7 +87,7 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
kind: "request_item_verdicts", status: "answered", resolvedAt: new Date(),
|
||||
payload: { version: 1, prompt: "Review", items: [] },
|
||||
result: { version: 1, outcome: "resolved", complete: true,
|
||||
items: verdicts.map((verdict, index) => ({ id: String(index), verdict })) } });
|
||||
items: verdicts.map((verdict, index) => ({ id: String(index), verdict, resolvedAt: new Date().toISOString() })) } });
|
||||
}
|
||||
await db.insert(issueThreadInteractions).values([
|
||||
{ companyId, issueId, title: "Question answered", kind: "ask_user_questions", status: "answered", resolvedAt: new Date(), payload: { version: 1, questions: [] } },
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
issues,
|
||||
} from "@paperclipai/db";
|
||||
import type { AttentionItem } from "@paperclipai/shared";
|
||||
import { DEFAULT_DECISION_SHELF_DAYS } from "../services/decision-retention.js";
|
||||
import {
|
||||
getEmbeddedPostgresTestSupport,
|
||||
startEmbeddedPostgresTestDatabase,
|
||||
@@ -323,6 +324,18 @@ describeEmbeddedPostgres("decision queue routes", () => {
|
||||
} : null,
|
||||
project: null,
|
||||
workspace: null,
|
||||
expiresAt: null,
|
||||
ruleKey: null,
|
||||
originAgentName: null,
|
||||
queues: [],
|
||||
shelf: false,
|
||||
retentionDays: DEFAULT_DECISION_SHELF_DAYS,
|
||||
keep: false,
|
||||
archivedAt: null,
|
||||
retentionVersion: 0,
|
||||
decideBy: null,
|
||||
decideByAttribution: null,
|
||||
snoozedUntil: null,
|
||||
detail: null,
|
||||
trainingExampleId: null,
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
|
||||
import { attentionService } from "../services/attention.js";
|
||||
import { decisionService } from "../services/decisions.js";
|
||||
import { signDecisionSpec } from "../services/decision-signing.js";
|
||||
import * as retentionModule from "../services/decision-retention.js";
|
||||
import { hashAttentionArchiveManifest } from "../services/decision-retention.js";
|
||||
import type { AttentionArchiveManifestEntry, AttentionArchiveTargetSnapshot } from "@paperclipai/shared";
|
||||
@@ -395,9 +396,12 @@ describePg("decisionService", () => {
|
||||
const created = await service().create({
|
||||
companyId, actor: agentActor(), agentId, runId, title: "Cancel?", body: "Body",
|
||||
options: [{ id: "yes", label: "Yes", effects: [{
|
||||
type: "cancel_issue_tree", targetIssueId, staleness: "lenient", reasonComment: "cleanup",
|
||||
type: "cancel_issue_tree", targetIssueId, staleness: "strict", reasonComment: "cleanup",
|
||||
}] }],
|
||||
});
|
||||
// The effect type admits only strict cancellation, so a lenient one can exist only as a stored, signed row.
|
||||
const [stored] = await db.update(decisions).set({ options: sql`jsonb_set(${decisions.options}, '{0,effects,0,staleness}', '"lenient"')` }).where(eq(decisions.id, created.id)).returning();
|
||||
await db.update(decisions).set({ signedSpec: signDecisionSpec({ decisionId: stored!.id, options: stored!.options, targetSnapshots: stored!.targetSnapshots }) }).where(eq(decisions.id, created.id));
|
||||
const unreviewedChildId = randomUUID();
|
||||
await db.insert(issues).values({ id: unreviewedChildId, companyId, title: "Unreviewed child", status: "todo", priority: "medium",
|
||||
parentId: targetIssueId, responsibleUserId: decidedByUserId });
|
||||
|
||||
@@ -213,7 +213,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
|
||||
it("delivers reviewed REST actions through native assignments without admitting unsupported or inactive connections", async () => {
|
||||
vi.stubEnv("PAPERCLIP_API_URL", "http://127.0.0.1:3100");
|
||||
onTestFinished(() => vi.unstubAllEnvs());
|
||||
onTestFinished(() => { vi.unstubAllEnvs(); });
|
||||
const f = await fixture(provider, true);
|
||||
await f.service.putConnectionInstalls(f.connected.connectionId, {
|
||||
installs: [{ targetType: "agent", targetId: f.agent.id }],
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("paperclip runner native dev prerequisite", () => {
|
||||
Object.defineProperty(process, "arch", { ...initialArchitecture, value: architecture });
|
||||
const nativeRunnerRequired = paperclipRunnerSupportsPlatform("codex_local", "linux", architecture, glibcVersionRuntime ? "glibc" : "unknown", glibcVersionRuntime as string | undefined);
|
||||
expect(nativeRunnerRequired).toBe(native);
|
||||
expect(paperclipRunnerBinaryNeedsBuild({ repoRoot: "/checkout/without/a/staged/binary", nativeRunnerRequired })).toBe(native);
|
||||
expect(paperclipRunnerBinaryNeedsBuild({ repoRoot: "/checkout/without/a/staged/binary", nativeRunnerRequired, configuredBinary: undefined })).toBe(native);
|
||||
});
|
||||
it("uses an explicit status response and fails safe when status is unknown", () => {
|
||||
expect(
|
||||
@@ -109,6 +109,7 @@ describe("paperclip runner native dev prerequisite", () => {
|
||||
paperclipRunnerBinaryNeedsBuild({
|
||||
repoRoot: checkout.root,
|
||||
nativeRunnerRequired: true,
|
||||
configuredBinary: undefined,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
@@ -117,6 +118,7 @@ describe("paperclip runner native dev prerequisite", () => {
|
||||
paperclipRunnerBinaryNeedsBuild({
|
||||
repoRoot: checkout.root,
|
||||
nativeRunnerRequired: true,
|
||||
configuredBinary: undefined,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
@@ -125,6 +127,7 @@ describe("paperclip runner native dev prerequisite", () => {
|
||||
paperclipRunnerBinaryNeedsBuild({
|
||||
repoRoot: checkout.root,
|
||||
nativeRunnerRequired: true,
|
||||
configuredBinary: undefined,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -134,6 +137,7 @@ describe("paperclip runner native dev prerequisite", () => {
|
||||
paperclipRunnerBinaryNeedsBuild({
|
||||
repoRoot: "/checkout/without/a/staged/binary",
|
||||
nativeRunnerRequired: false,
|
||||
configuredBinary: undefined,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
@@ -148,14 +152,14 @@ describe("paperclip runner native dev prerequisite", () => {
|
||||
const now = Date.now(), old = new Date(now - 2_000), built = new Date(now + 2_000), changed = new Date(now + 4_000);
|
||||
fs.utimesSync(dependency, old, old);
|
||||
fs.utimesSync(checkout.binary, built, built);
|
||||
const options = { repoRoot: checkout.root, nativeRunnerRequired: true };
|
||||
const options = { repoRoot: checkout.root, nativeRunnerRequired: true, configuredBinary: undefined };
|
||||
expect(paperclipRunnerBinaryNeedsBuild(options)).toBe(false);
|
||||
fs.utimesSync(dependency, changed, changed);
|
||||
expect(paperclipRunnerBinaryNeedsBuild(options)).toBe(true);
|
||||
expect(paperclipRunnerBinaryNeedsBuild({ ...options, nativeRunnerRequired: false })).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["arm64", "x64"])("uses the staged Mac %s target before the flat build output", architecture => {
|
||||
it.each(["arm64", "x64"] as const)("uses the staged Mac %s target before the flat build output", architecture => {
|
||||
const checkout = createRunnerCheckout();
|
||||
const target = path.join(path.dirname(checkout.binary), `darwin-${architecture}`, "paperclip-runnerd");
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
@@ -163,7 +167,7 @@ describe("paperclip runner native dev prerequisite", () => {
|
||||
const old = new Date(Date.now() - 2000), current = new Date(Date.now() + 2000);
|
||||
fs.utimesSync(checkout.binary, old, old);
|
||||
fs.utimesSync(target, current, current);
|
||||
const input = { repoRoot: checkout.root, nativeRunnerRequired: true, platform: "darwin", architecture };
|
||||
const input = { repoRoot: checkout.root, nativeRunnerRequired: true, configuredBinary: undefined, platform: "darwin" as const, architecture };
|
||||
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
|
||||
fs.writeFileSync(checkout.binary, nativeHeader("linux", "x64"));
|
||||
const wrongHostNewer = new Date(Date.now() + 4000);
|
||||
@@ -178,7 +182,7 @@ describe("paperclip runner native dev prerequisite", () => {
|
||||
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["arm64", "x64"])("uses only a newer matching Mac %s source build without changing release selection", architecture => {
|
||||
it.each(["arm64", "x64"] as const)("uses only a newer matching Mac %s source build without changing release selection", architecture => {
|
||||
const checkout = createRunnerCheckout();
|
||||
const target = path.join(path.dirname(checkout.binary), `darwin-${architecture}`, "paperclip-runnerd");
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
@@ -186,7 +190,7 @@ describe("paperclip runner native dev prerequisite", () => {
|
||||
fs.writeFileSync(checkout.binary, nativeHeader("darwin", architecture));
|
||||
const old = new Date(Date.now() - 2000), fresh = new Date(Date.now() + 2000), changed = new Date(Date.now() + 4000);
|
||||
fs.utimesSync(target, old, old); fs.utimesSync(checkout.binary, fresh, fresh);
|
||||
const input = { repoRoot: checkout.root, nativeRunnerRequired: true, platform: "darwin", architecture };
|
||||
const input = { repoRoot: checkout.root, nativeRunnerRequired: true, configuredBinary: undefined, platform: "darwin" as const, architecture };
|
||||
expect(resolveNativeRunnerDevBinary(input)).toEqual({ binary: checkout.binary, sourceBuild: true });
|
||||
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
|
||||
fs.utimesSync(checkout.source, changed, changed);
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("createCapturedOutputBuffer", () => {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
|
||||
await expect(parseJsonResponseWithLimit<{ ok: boolean }>(response, 64)).resolves.toEqual({ ok: true });
|
||||
await expect(parseJsonResponseWithLimit(response, 64)).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("rejects oversized JSON responses before parsing them", async () => {
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("dev-runner watched snapshot", () => {
|
||||
|
||||
const fileSystem = {
|
||||
existsSync: fs.existsSync,
|
||||
readdirSync(target: fs.PathLike, options?: fs.ObjectEncodingOptions & { withFileTypes: true }) {
|
||||
readdirSync(target: fs.PathLike, options: fs.ObjectEncodingOptions & { withFileTypes: true }) {
|
||||
if (target === routesDir) {
|
||||
fs.rmSync(routesDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -84,6 +84,8 @@ describe("dev-runner watched snapshot", () => {
|
||||
const root = createTempRoot("paperclip-dev-runner-snapshot-diff-");
|
||||
const missingPath = path.join(root, "server", "deleted.ts");
|
||||
const fileSystem = {
|
||||
existsSync: fs.existsSync,
|
||||
readdirSync: fs.readdirSync,
|
||||
statSync() {
|
||||
const error = new Error("not a directory") as NodeJS.ErrnoException;
|
||||
error.code = "ENOTDIR";
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
writeDevServerRestartRequest,
|
||||
} from "../dev-server-status.js";
|
||||
|
||||
const tempDirs = [];
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function createTempStatusFile(payload: unknown) {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "paperclip-dev-status-"));
|
||||
|
||||
@@ -30,6 +30,7 @@ function createMemoryReaperStore(): AdapterAuthReaperStore & {
|
||||
seed(input) {
|
||||
const row: AdapterAuthSessionRow = {
|
||||
id: input.id,
|
||||
publicSessionId: input.publicSessionId ?? randomUUID(),
|
||||
companyId: input.companyId ?? randomUUID(),
|
||||
environmentId: input.environmentId ?? randomUUID(),
|
||||
adapterType: input.adapterType ?? ADAPTER_TYPE,
|
||||
@@ -40,6 +41,7 @@ function createMemoryReaperStore(): AdapterAuthReaperStore & {
|
||||
promotionExpiresAt: input.promotionExpiresAt ?? null,
|
||||
finishedAt: input.finishedAt ?? null,
|
||||
failureReason: input.failureReason ?? null,
|
||||
resultClaim: input.resultClaim ?? null,
|
||||
};
|
||||
rows.set(row.id, row);
|
||||
return row;
|
||||
|
||||
@@ -218,6 +218,7 @@ function createMemoryStore(): AdapterAuthSessionStore & {
|
||||
promotionExpiresAt: null,
|
||||
finishedAt: null,
|
||||
failureReason: null,
|
||||
resultClaim: null,
|
||||
});
|
||||
},
|
||||
async recordLeaseAcquired(input) {
|
||||
@@ -1413,7 +1414,7 @@ describeEmbeddedPostgres("device login service concurrency (embedded postgres)",
|
||||
|
||||
beforeAll(async () => {
|
||||
const started = await startEmbeddedPostgresTestDatabase("device-login");
|
||||
stopDb = started.stop;
|
||||
stopDb = started.cleanup;
|
||||
db = createDb(started.connectionString);
|
||||
});
|
||||
|
||||
@@ -2039,7 +2040,7 @@ describeEmbeddedPostgres("device login service concurrency (embedded postgres)",
|
||||
// The start ttl read: `session.expiresAt` reflects the profile's
|
||||
// `timeoutMs`, not the fixed `DEVICE_LOGIN_TIMEOUT_MS`.
|
||||
const expectedExpiresAt = Date.now() + customTimeoutMs;
|
||||
expect(Math.abs(new Date(started.session.expiresAt).getTime() - expectedExpiresAt)).toBeLessThan(
|
||||
expect(Math.abs(new Date(started.session.expiresAt!).getTime() - expectedExpiresAt)).toBeLessThan(
|
||||
5000,
|
||||
);
|
||||
|
||||
|
||||
@@ -37,12 +37,12 @@ const mockIssueReferenceService = vi.hoisted(() => ({
|
||||
syncIssue: vi.fn(async () => undefined),
|
||||
}));
|
||||
const mockHeartbeatService = vi.hoisted(() => ({
|
||||
wakeup: vi.fn(async () => undefined),
|
||||
wakeup: vi.fn(async (): Promise<{ id: string } | undefined> => undefined),
|
||||
reportRunActivity: vi.fn(async () => undefined),
|
||||
}));
|
||||
const mockIssueThreadInteractionService = vi.hoisted(() => ({
|
||||
expireRequestConfirmationsSupersededByComment: vi.fn(async () => []),
|
||||
expireStaleRequestConfirmationsForIssueDocument: vi.fn(async () => []),
|
||||
expireStaleRequestConfirmationsForIssueDocument: vi.fn(async (): Promise<Array<{ id: string; kind: string; status: string }>> => []),
|
||||
}));
|
||||
const mockLogActivity = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("Dot onboarding with an operator-issued pairing capability", () => {
|
||||
}, 120000);
|
||||
beforeEach(async () => { await instanceSettingsService(db).updateExperimental({ enablePublicMcp: true, enableOpenAiDot: true, enableNativeRunner: false }); });
|
||||
afterAll(async () => { await temp?.cleanup(); });
|
||||
async function fixture(authorizationOrigin?: string, lifecycleState = "ready") {
|
||||
async function fixture(authorizationOrigin?: string, lifecycleState: NonNullable<typeof agents.$inferInsert["lifecycleState"]> = "ready") {
|
||||
const userId = randomUUID();
|
||||
await db.insert(authUsers).values({ id: userId, name: "Operator", email: userId + "@example.test", createdAt: new Date(), updatedAt: new Date() });
|
||||
const [company] = await db.insert(companies).values({ name: "Dot onboarding", issuePrefix: "DO" + randomBytes(3).toString("hex") }).returning();
|
||||
@@ -64,7 +64,7 @@ describe("Dot onboarding with an operator-issued pairing capability", () => {
|
||||
expect(revoked.lifecycleVersion).toBe(paired.lifecycleVersion + 1);
|
||||
await expect(f.oauth.authenticate(tokens.access_token)).rejects.toThrow();
|
||||
});
|
||||
it.each(["preparing", "verifying"])("allows Dot setup during %s without allowing task access", async (state) => {
|
||||
it.each(["preparing", "verifying"] as const)("allows Dot setup during %s without allowing task access", async (state) => {
|
||||
const f = await fixture(undefined, state);
|
||||
const tokens = await connect(f);
|
||||
const principal = await f.oauth.authenticate(tokens.access_token);
|
||||
|
||||
@@ -24,7 +24,8 @@ import { startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.j
|
||||
import { createPublicMcpOAuth, DEVICE_GRANT } from "../services/public-mcp/oauth.js";
|
||||
import { createPublicMcpExecutor } from "../services/public-mcp/capabilities.js";
|
||||
import { publicMcpIngressRoutes, publicMcpManagementRoutes } from "../routes/public-mcp.js";
|
||||
import { createPublicMcpEvents, type EventFetch } from "../services/public-mcp/events.js";
|
||||
import { createPublicMcpEvents } from "../services/public-mcp/events.js";
|
||||
import type { EventFetch } from "../services/public-mcp/event-webhooks.js";
|
||||
import { instanceSettingsService } from "../services/instance-settings.js";
|
||||
import { canonicalNativeRuntimeContextDigest, type StrictCompletionContractInput } from "../vendor/paperclip-runner/index.js";
|
||||
import { createDotRunnerMcpTools, dotRunnerBroker } from "../services/dot-runner-broker.js";
|
||||
@@ -46,6 +47,11 @@ import { createReleaseIssueExecution } from "../modules/wake-queue/application/u
|
||||
|
||||
const externalUrl = resolveExternalTestDatabaseUrl("PAPERCLIP_DOT_RUNNER_TEST_DATABASE_URL");
|
||||
|
||||
function uploadGrantUrl(grant: { url: string } | { attachment: unknown }) {
|
||||
if (!("url" in grant)) throw new Error("Expected an upload grant");
|
||||
return new URL(grant.url);
|
||||
}
|
||||
|
||||
describe("durable Dot Runner integration", () => {
|
||||
let temporary: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
|
||||
let db: ReturnType<typeof createDb>;
|
||||
@@ -264,7 +270,7 @@ describe("durable Dot Runner integration", () => {
|
||||
const body = Buffer.from("Authenticated concurrent deliverable"), uploadRequestId = randomUUID(), operationId = randomUUID();
|
||||
const upload = await transfers.getUploadUrl(f.principal, { companyId: f.company.id, taskId: issue!.id, requestId: uploadRequestId,
|
||||
filename: "concurrent.txt", contentType: "text/plain", byteSize: body.length, sha256: createHash("sha256").update(body).digest("hex") }, assignment.id);
|
||||
const url = new URL(String(upload.url)), api = express(); api.use(transfers.router);
|
||||
const url = uploadGrantUrl(upload), api = express(); api.use(transfers.router);
|
||||
let uploading: Promise<request.Response> | undefined, uploaded: request.Response | undefined, uploadError: unknown;
|
||||
try {
|
||||
await db.transaction(async tx => {
|
||||
@@ -961,7 +967,7 @@ describe("durable Dot Runner integration", () => {
|
||||
const [assignment] = await db.select().from(dotRunnerAssignments).where(eq(dotRunnerAssignments.runId, run!.id));
|
||||
if (provider === "openai_dot") await f.events.tick();
|
||||
expect(JSON.stringify(f.received)).not.toContain("17 + 25");
|
||||
const work = await f.broker.read(f.principal, assignment!.id);
|
||||
const work = await f.broker.read(f.principal, assignment!.id) as Awaited<ReturnType<typeof f.broker.read>> & { tools: unknown };
|
||||
expect(work.accounting).toEqual({ usage: null, cost: null });
|
||||
expect(work.status).toBe("offered");
|
||||
expect(observed.filter(event => event.eventType === "turn.started")).toHaveLength(0);
|
||||
@@ -990,7 +996,7 @@ describe("durable Dot Runner integration", () => {
|
||||
const body = Buffer.from("Grok arithmetic file: 42\n"), uploadId = randomUUID();
|
||||
const args = { companyId: f.company.id, taskId: issue!.id, requestId: uploadId, filename: "report.txt", contentType: "text/plain", byteSize: body.length, sha256: createHash("sha256").update(body).digest("hex") };
|
||||
const grant = await transfers.getUploadUrl(f.principal, args, assignment!.id);
|
||||
const url = new URL(String(grant.url));
|
||||
const url = uploadGrantUrl(grant);
|
||||
if (revokeDuringUpload) {
|
||||
const uploading = request(api).put(url.pathname + url.search).set("Content-Type", "text/plain").send(body)
|
||||
.then(response => response);
|
||||
@@ -1077,7 +1083,7 @@ describe("durable Dot Runner integration", () => {
|
||||
await expect(transfers.getUploadUrl(f.principal, { ...args, taskId: randomUUID() }, assignment!.id)).rejects.toThrow();
|
||||
const download = new URL(String((await transfers.getDownloadUrl(f.principal, uploaded.body.attachment.id, assignment!.id)).url));
|
||||
expect((await request(api).get(download.pathname + download.search)).status).toBe(200);
|
||||
const pendingUpload = new URL(String((await transfers.getUploadUrl(f.principal, { ...args, requestId: randomUUID() }, assignment!.id)).url));
|
||||
const pendingUpload = uploadGrantUrl(await transfers.getUploadUrl(f.principal, { ...args, requestId: randomUUID() }, assignment!.id));
|
||||
await db.update(issues).set({ workMode: "ask" }).where(eq(issues.id, issue!.id));
|
||||
await expect(transfers.getUploadUrl(f.principal, { ...args, requestId: randomUUID() }, assignment!.id)).rejects.toThrow("work mode");
|
||||
expect((await request(api).put(pendingUpload.pathname + pendingUpload.search).set("Content-Type", "text/plain").send(body)).status).toBeGreaterThanOrEqual(400);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
DUPLEX_SPAN_CHANNEL_OPEN,
|
||||
DUPLEX_SPAN_REQUEST,
|
||||
DUPLEX_TRANSPORT_EVENT,
|
||||
type DuplexObservabilityDimensions,
|
||||
} from "@paperclipai/adapter-utils/duplex-observability";
|
||||
import { runWithRuntimeParent } from "@paperclipai/adapter-utils/acpx-engine/startup-timing";
|
||||
import {
|
||||
@@ -180,7 +181,7 @@ describe("createHostDuplexObservabilityRecorder", () => {
|
||||
});
|
||||
|
||||
it("forwards the transport event with its name and dimensions", () => {
|
||||
const events: Array<{ name: string; dimensions: Record<string, unknown> }> =
|
||||
const events: Array<{ name: string; dimensions: DuplexObservabilityDimensions }> =
|
||||
[];
|
||||
const recorder = createHostDuplexObservabilityRecorder({
|
||||
tracer: createRecordingTracer().tracer,
|
||||
|
||||
@@ -183,7 +183,7 @@ describe("durable inbound chat scheduler receipts", () => {
|
||||
wakeCommentId: durableChatRequest.commentId,
|
||||
durableChatRequest,
|
||||
rethrowOnError: true,
|
||||
});
|
||||
}) as Promise<Awaited<ReturnType<typeof heartbeat.wakeup>>> | undefined;
|
||||
return {
|
||||
companyId,
|
||||
agentId,
|
||||
|
||||
@@ -217,7 +217,7 @@ describe("AgentMail durable email pipeline", () => {
|
||||
});
|
||||
|
||||
async function fixture(mode: "websocket" | "webhook" = "webhook", storage?: StorageService,
|
||||
beforeSetup?: (input: { companyId: string; fetcher: typeof fetch; wakeup: ReturnType<typeof vi.fn> }) => Promise<void>,
|
||||
beforeSetup?: (input: { companyId: string; fetcher: typeof fetch; wakeup: Parameters<typeof emailChannelService>[1]["heartbeat"]["wakeup"] }) => Promise<void>,
|
||||
) {
|
||||
const companyId = randomUUID(),
|
||||
agentId = randomUUID(),
|
||||
@@ -344,8 +344,8 @@ describe("AgentMail durable email pipeline", () => {
|
||||
{ userId: "email-board" },
|
||||
);
|
||||
function message(
|
||||
id = randomUUID(),
|
||||
thread = randomUUID(),
|
||||
id: string = randomUUID(),
|
||||
thread: string = randomUUID(),
|
||||
extra: Partial<AgentmailMessage> = {},
|
||||
) {
|
||||
return agentmailMessageSchema.parse({
|
||||
@@ -1318,7 +1318,7 @@ describe("AgentMail durable email pipeline", () => {
|
||||
await db.insert(principalPermissionGrants).values({ companyId: f.companyId, principalType: "user",
|
||||
principalId: actor.userId, permissionKey: "tools:manage_connections" });
|
||||
expect(await accessService(db).hasPermission(f.companyId, "user", actor.userId, "tools:manage_connections")).toBe(true);
|
||||
expect(await accessService(db).hasPermission(f.companyId, "user", actor.userId, "agent_config:update")).toBe(false);
|
||||
expect(await accessService(db).hasPermission(f.companyId, "user", actor.userId, "agents:configure")).toBe(false);
|
||||
const requestId = randomUUID();
|
||||
const svc = emailConnectionService(db, f.fetcher);
|
||||
const connection = await svc.connect(f.companyId, { apiKey: "test-key", grantKind: "organization",
|
||||
|
||||
@@ -14,8 +14,9 @@ function fixture() {
|
||||
vi.useFakeTimers();
|
||||
const select = vi.fn(() => ({ from: () => ({ where: async () => [] }) }));
|
||||
const execute = vi.fn(async () => [{ xid: "42", status: "aborted" }]);
|
||||
type FixtureTx = { execute: typeof execute; transaction: <T>(callback: (tx: FixtureTx) => Promise<T>) => Promise<T> };
|
||||
const db = installDatabaseWorkSignals({ select, execute,
|
||||
async transaction(callback: (tx: any) => Promise<any>) {
|
||||
async transaction<T>(callback: (tx: FixtureTx) => Promise<T>): Promise<T> {
|
||||
return callback({ execute, transaction: db.transaction });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -186,11 +186,11 @@ function createSession(overrides: Record<string, unknown> = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function createApp(actor: Record<string, unknown>) {
|
||||
function createApp(actor: Express.Request["actor"]) {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => {
|
||||
(req as typeof req & { actor: Record<string, unknown> }).actor = actor;
|
||||
req.actor = actor;
|
||||
next();
|
||||
});
|
||||
app.use("/api", environmentRoutes({} as never));
|
||||
@@ -202,7 +202,7 @@ function createApp(actor: Record<string, unknown>) {
|
||||
return app;
|
||||
}
|
||||
|
||||
function boardActor(overrides: Record<string, unknown> = {}) {
|
||||
function boardActor(overrides: Partial<Express.Request["actor"]> = {}): Express.Request["actor"] {
|
||||
return {
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
@@ -213,7 +213,7 @@ function boardActor(overrides: Record<string, unknown> = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function agentActor() {
|
||||
function agentActor(): Express.Request["actor"] {
|
||||
return {
|
||||
type: "agent",
|
||||
agentId: "agent-1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { createServer, type Server as HttpServer } from "node:http";
|
||||
import { createRequire } from "node:module";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
||||
|
||||
vi.mock("../services/environment-custom-images.js", () => ({
|
||||
environmentCustomImageService: vi.fn(() => {
|
||||
@@ -78,6 +78,10 @@ function futureDate(minutes = 60) {
|
||||
return new Date(Date.now() + minutes * 60 * 1000);
|
||||
}
|
||||
|
||||
type CustomImageTerminalService = NonNullable<
|
||||
NonNullable<Parameters<typeof setupEnvironmentCustomImageTerminalWebSocketServer>[2]>["customImageService"]
|
||||
>;
|
||||
|
||||
function createSession(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "session-1",
|
||||
@@ -178,8 +182,8 @@ describe("custom image terminal websocket bridge", () => {
|
||||
let connectionRegistry: EnvironmentCustomImageTerminalConnectionRegistry;
|
||||
let fakeShell: FakeSshShell;
|
||||
let customImages: {
|
||||
getSessionById: ReturnType<typeof vi.fn>;
|
||||
refreshSetupSession: ReturnType<typeof vi.fn>;
|
||||
getSessionById: Mock<CustomImageTerminalService["getSessionById"]>;
|
||||
refreshSetupSession: Mock<CustomImageTerminalService["refreshSetupSession"]>;
|
||||
};
|
||||
let connector: EnvironmentCustomImageSshConnector & { connect: ReturnType<typeof vi.fn> };
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
environments,
|
||||
plugins,
|
||||
} from "@paperclipai/db";
|
||||
import type { PaperclipPluginManifestV1 } from "@paperclipai/shared";
|
||||
import type { PluginWorkerManager } from "../services/plugin-worker-manager.js";
|
||||
import {
|
||||
environmentCustomImageService,
|
||||
@@ -47,6 +48,8 @@ function pluginManifest() {
|
||||
apiVersion: 1,
|
||||
version: "0.1.0",
|
||||
displayName: "Fake Sandbox Provider",
|
||||
description: "Test fake sandbox provider",
|
||||
author: "Paperclip",
|
||||
categories: ["automation"],
|
||||
capabilities: ["environment.drivers.register"],
|
||||
entrypoints: { worker: "./dist/worker.js" },
|
||||
@@ -68,7 +71,7 @@ function pluginManifest() {
|
||||
configSchema: { type: "object" },
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
} satisfies PaperclipPluginManifestV1;
|
||||
}
|
||||
|
||||
const ALL_CUSTOM_IMAGE_WORKER_METHODS = [
|
||||
@@ -1229,6 +1232,8 @@ function secretPluginManifest() {
|
||||
apiVersion: 1,
|
||||
version: "0.1.0",
|
||||
displayName: "Fake Secret Sandbox Provider",
|
||||
description: "Test fake sandbox provider",
|
||||
author: "Paperclip",
|
||||
categories: ["automation"],
|
||||
capabilities: ["environment.drivers.register"],
|
||||
entrypoints: { worker: "./dist/worker.js" },
|
||||
@@ -1259,7 +1264,7 @@ function secretPluginManifest() {
|
||||
},
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
} satisfies PaperclipPluginManifestV1;
|
||||
}
|
||||
|
||||
function invalidIdentityPluginManifest() {
|
||||
@@ -1268,6 +1273,8 @@ function invalidIdentityPluginManifest() {
|
||||
apiVersion: 1,
|
||||
version: "0.1.0",
|
||||
displayName: "Fake Invalid Identity Sandbox Provider",
|
||||
description: "Test fake sandbox provider",
|
||||
author: "Paperclip",
|
||||
categories: ["automation"],
|
||||
capabilities: ["environment.drivers.register"],
|
||||
entrypoints: { worker: "./dist/worker.js" },
|
||||
@@ -1287,7 +1294,7 @@ function invalidIdentityPluginManifest() {
|
||||
configSchema: { type: "object" },
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
} satisfies PaperclipPluginManifestV1;
|
||||
}
|
||||
|
||||
function prototypeKeyIdentityPluginManifest() {
|
||||
@@ -1296,6 +1303,8 @@ function prototypeKeyIdentityPluginManifest() {
|
||||
apiVersion: 1,
|
||||
version: "0.1.0",
|
||||
displayName: "Fake Prototype Key Sandbox Provider",
|
||||
description: "Test fake sandbox provider",
|
||||
author: "Paperclip",
|
||||
categories: ["automation"],
|
||||
capabilities: ["environment.drivers.register"],
|
||||
entrypoints: { worker: "./dist/worker.js" },
|
||||
@@ -1316,7 +1325,7 @@ function prototypeKeyIdentityPluginManifest() {
|
||||
configSchema: { type: "object" },
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
} satisfies PaperclipPluginManifestV1;
|
||||
}
|
||||
|
||||
describeEmbeddedPostgres("environmentCustomImageService relink", () => {
|
||||
|
||||
@@ -131,7 +131,7 @@ describe("sandbox driver duplex channel wiring", () => {
|
||||
const channel = await service.openDuplexChannel({
|
||||
environment: SANDBOX_ENVIRONMENT,
|
||||
lease: PLUGIN_LEASE,
|
||||
command: "bridge-callback",
|
||||
command: ["bridge-callback"],
|
||||
});
|
||||
|
||||
// The driver resolves the worker by the pinned plugin id and passes the same
|
||||
@@ -142,7 +142,7 @@ describe("sandbox driver duplex channel wiring", () => {
|
||||
companyId: "company-1",
|
||||
environmentId: "env-1",
|
||||
providerLeaseId: "provider-lease-1",
|
||||
command: "bridge-callback",
|
||||
command: ["bridge-callback"],
|
||||
});
|
||||
|
||||
// write / stop / close map to write / kill / close on the host session.
|
||||
@@ -185,7 +185,7 @@ describe("sandbox driver duplex channel wiring", () => {
|
||||
service.openDuplexChannel({
|
||||
environment: SANDBOX_ENVIRONMENT,
|
||||
lease: nonPluginLease,
|
||||
command: "bridge-callback",
|
||||
command: ["bridge-callback"],
|
||||
}),
|
||||
).rejects.toThrow(DUPLEX_DENIED);
|
||||
expect(worker.openDuplexChannel).not.toHaveBeenCalled();
|
||||
@@ -210,7 +210,7 @@ describe("EnvironmentRuntimeService.openDuplexChannel capability gate", () => {
|
||||
service.openDuplexChannel({
|
||||
environment: SANDBOX_ENVIRONMENT,
|
||||
lease: PLUGIN_LEASE,
|
||||
command: "bridge-callback",
|
||||
command: ["bridge-callback"],
|
||||
}),
|
||||
).rejects.toThrow(DUPLEX_DENIED);
|
||||
expect(worker.openDuplexChannel).not.toHaveBeenCalled();
|
||||
@@ -228,7 +228,7 @@ describe("EnvironmentRuntimeService.openDuplexChannel capability gate", () => {
|
||||
service.openDuplexChannel({
|
||||
environment: SANDBOX_ENVIRONMENT,
|
||||
lease: PLUGIN_LEASE,
|
||||
command: "bridge-callback",
|
||||
command: ["bridge-callback"],
|
||||
}),
|
||||
).rejects.toThrow(DUPLEX_DENIED);
|
||||
expect(worker.openDuplexChannel).not.toHaveBeenCalled();
|
||||
@@ -253,7 +253,7 @@ describe("EnvironmentRuntimeService.openDuplexChannel capability gate", () => {
|
||||
service.openDuplexChannel({
|
||||
environment: SANDBOX_ENVIRONMENT,
|
||||
lease: { ...PLUGIN_LEASE, metadata: { ...PLUGIN_LEASE.metadata, driver: "sandbox" } },
|
||||
command: "bridge-callback",
|
||||
command: ["bridge-callback"],
|
||||
}),
|
||||
).rejects.toThrow(DUPLEX_DENIED);
|
||||
expect(openDuplexChannel).not.toHaveBeenCalled();
|
||||
@@ -271,7 +271,7 @@ describe("EnvironmentRuntimeService.openDuplexChannel capability gate", () => {
|
||||
service.openDuplexChannel({
|
||||
environment: SANDBOX_ENVIRONMENT,
|
||||
lease: PLUGIN_LEASE,
|
||||
command: "bridge-callback",
|
||||
command: ["bridge-callback"],
|
||||
}),
|
||||
).rejects.toThrow(DUPLEX_DENIED);
|
||||
expect(worker.openDuplexChannel).not.toHaveBeenCalled();
|
||||
@@ -353,11 +353,11 @@ describe("inline sandbox runner duplex capability gate", () => {
|
||||
const { runner, openDuplexChannel } = await buildSandboxRunner({ snapshot: DUPLEX_GRANT });
|
||||
expect(runner?.openDuplexChannel).toBeDefined();
|
||||
|
||||
await runner!.openDuplexChannel!({ command: "bridge-callback" });
|
||||
await runner!.openDuplexChannel!({ command: ["bridge-callback"] });
|
||||
expect(openDuplexChannel).toHaveBeenCalledWith({
|
||||
environment: expect.objectContaining({ id: "env-1" }),
|
||||
lease: expect.objectContaining({ id: "lease-1" }),
|
||||
command: "bridge-callback",
|
||||
command: ["bridge-callback"],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loaded 100 of 345 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user