diff --git a/backend/internal/httpd/apispec/openapi.yaml b/backend/internal/httpd/apispec/openapi.yaml index a66191e55..2a3de1a9b 100644 --- a/backend/internal/httpd/apispec/openapi.yaml +++ b/backend/internal/httpd/apispec/openapi.yaml @@ -51,6 +51,55 @@ paths: summary: Stream CDC events with durable replay tags: - events + /api/v1/import: + get: + operationId: getImportStatus + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportStatusResponse' + description: OK + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/APIError' + description: Internal Server Error + "501": + content: + application/json: + schema: + $ref: '#/components/schemas/APIError' + description: Not Implemented + summary: Check whether a legacy AO install is available to import + tags: + - import + post: + operationId: runImport + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ImportRunResponse' + description: OK + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/APIError' + description: Internal Server Error + "501": + content: + application/json: + schema: + $ref: '#/components/schemas/APIError' + description: Not Implemented + summary: Run the legacy AO project import through the daemon store + tags: + - import /api/v1/notifications: get: operationId: listNotifications @@ -1600,6 +1649,40 @@ components: required: - harness type: object + ImportReport: + properties: + dryRun: + type: boolean + notes: + items: + type: string + type: array + projectsImported: + type: integer + projectsSkipped: + type: integer + required: + - dryRun + - projectsImported + - projectsSkipped + type: object + ImportRunResponse: + properties: + report: + $ref: '#/components/schemas/ImportReport' + required: + - report + type: object + ImportStatusResponse: + properties: + available: + type: boolean + legacyRoot: + type: string + required: + - available + - legacyRoot + type: object KillSessionResponse: properties: freed: @@ -2429,3 +2512,5 @@ tags: name: notifications - description: Server-sent CDC event stream with durable replay name: events +- description: Legacy AO project import (availability probe and run) + name: import diff --git a/backend/internal/httpd/apispec/specgen/build.go b/backend/internal/httpd/apispec/specgen/build.go index a2611d61f..842a882ef 100644 --- a/backend/internal/httpd/apispec/specgen/build.go +++ b/backend/internal/httpd/apispec/specgen/build.go @@ -67,6 +67,8 @@ func Build() ([]byte, error) { "Durable dashboard notifications"), *(&openapi31.Tag{Name: "events"}).WithDescription( "Server-sent CDC event stream with durable replay"), + *(&openapi31.Tag{Name: "import"}).WithDescription( + "Legacy AO project import (availability probe and run)"), } for _, op := range operations() { @@ -186,6 +188,11 @@ var schemaNames = map[string]string{ "ControllersSubmitReviewInput": "SubmitReviewInput", // domain review entities "DomainReviewRun": "ReviewRun", + // httpd/controllers — import wire envelopes + "ControllersImportStatusResponse": "ImportStatusResponse", + "ControllersImportRunResponse": "ImportRunResponse", + // legacyimport report + "LegacyimportReport": "ImportReport", // service/project entities + DTOs "ProjectProject": "Project", "ProjectSummary": "ProjectSummary", @@ -273,9 +280,35 @@ func operations() []operation { ops = append(ops, prOperations()...) ops = append(ops, reviewOperations()...) ops = append(ops, notificationOperations()...) + ops = append(ops, importOperations()...) return ops } +// importOperations declares the 2 /import operations. Must stay 1:1 with +// the routes ImportController.Register mounts (enforced by the parity test). +func importOperations() []operation { + return []operation{ + { + method: http.MethodGet, path: "/api/v1/import", id: "getImportStatus", tag: "import", + summary: "Check whether a legacy AO install is available to import", + resps: []respUnit{ + {http.StatusOK, controllers.ImportStatusResponse{}}, + {http.StatusInternalServerError, envelope.APIError{}}, + {http.StatusNotImplemented, envelope.APIError{}}, + }, + }, + { + method: http.MethodPost, path: "/api/v1/import", id: "runImport", tag: "import", + summary: "Run the legacy AO project import through the daemon store", + resps: []respUnit{ + {http.StatusOK, controllers.ImportRunResponse{}}, + {http.StatusInternalServerError, envelope.APIError{}}, + {http.StatusNotImplemented, envelope.APIError{}}, + }, + }, + } +} + func notificationOperations() []operation { return []operation{ { diff --git a/backend/internal/httpd/controllers/imports_test.go b/backend/internal/httpd/controllers/imports_test.go index 4faab0d87..87c4db3be 100644 --- a/backend/internal/httpd/controllers/imports_test.go +++ b/backend/internal/httpd/controllers/imports_test.go @@ -88,10 +88,12 @@ func TestImportAPI_RunError(t *testing.T) { } } -// TestImportAPI_NilSvcReturns501 verifies the controller falls through to the -// 501 stub when no import service is wired. This test only passes after -// Task 4 regenerates openapi.yaml to include /api/v1/import; until then the -// apispec.NotImplemented call panics on the missing operation. -// Kept as a reminder; re-enable after api:spec regen. -// -// func TestImportAPI_NilSvcReturns501(t *testing.T) { ... } +func TestImportAPI_NilSvcReturns501(t *testing.T) { + log := slog.New(slog.NewTextHandler(io.Discard, nil)) + srv := httptest.NewServer(httpd.NewRouterWithControl(config.Config{}, log, nil, httpd.APIDeps{}, httpd.ControlDeps{})) + t.Cleanup(srv.Close) + body, status, _ := doRequest(t, srv, "GET", "/api/v1/import", "") + assertErrorCode(t, body, status, http.StatusNotImplemented, "NOT_IMPLEMENTED") + body, status, _ = doRequest(t, srv, "POST", "/api/v1/import", "") + assertErrorCode(t, body, status, http.StatusNotImplemented, "NOT_IMPLEMENTED") +} diff --git a/frontend/src/api/schema.ts b/frontend/src/api/schema.ts index a7f68302d..ff6d8c967 100644 --- a/frontend/src/api/schema.ts +++ b/frontend/src/api/schema.ts @@ -21,6 +21,24 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/import": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Check whether a legacy AO install is available to import */ + get: operations["getImportStatus"]; + put?: never; + /** Run the legacy AO project import through the daemon store */ + post: operations["runImport"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/notifications": { parameters: { query?: never; @@ -551,6 +569,19 @@ export interface components { DomainReviewerConfig: { harness: string; }; + ImportReport: { + dryRun: boolean; + notes?: string[]; + projectsImported: number; + projectsSkipped: number; + }; + ImportRunResponse: { + report: components["schemas"]["ImportReport"]; + }; + ImportStatusResponse: { + available: boolean; + legacyRoot: string; + }; KillSessionResponse: { freed?: boolean; ok: boolean; @@ -918,6 +949,82 @@ export interface operations { }; }; }; + getImportStatus: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ImportStatusResponse"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIError"]; + }; + }; + /** @description Not Implemented */ + 501: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIError"]; + }; + }; + }; + }; + runImport: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ImportRunResponse"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIError"]; + }; + }; + /** @description Not Implemented */ + 501: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIError"]; + }; + }; + }; + }; listNotifications: { parameters: { query?: {